Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Jason Notes

Ubuntu

開發環境安裝

git clone https://github.com/clvv/fasd
git clone https://github.com/junegunn/fzf
git clone https://github.com/sharkdp/fd
git clone https://github.com/cgdb/cgdb
git clone https://github.com/neovim/neovim.git
git clone https://github.com/BurntSushi/ripgrep
git clone https://github.com/ggreer/the_silver_searcher
git clone https://github.com/universal-ctags/ctags

選擇套件來源

sudo /usr/bin/software-properties-gtk

ubuntu 22.04 package

sudo apt-get install autoconf automake linux-headers-`uname -r` \
 libclang-dev p7zip guake p7zip-full liblzma-dev \
 indicator-multiload filezilla pidgin pcmanx-gtk2 gparted meld \
 speedcrunch vim ssh id-utils cflow autogen \
 cutecom hexedit ccache clang pbzip2 smplayer plink putty-tools \
 ghex doxygen doxygen-doc libstdc++6 lib32stdc++6 build-essential \
 doxygen-gui graphviz git-core cconv alsa-oss wmctrl terminator \
 curl cgdb dos2unix libreadline-dev tmux \
 hexedit ccache ruby subversion htop astyle ubuntu-restricted-extras \
 libncurses5-dev xdot universal-ctags cscope \
 libsdl1.2-dev gitk libncurses5-dev binutils-dev gtkterm \
 libtool mpi-default-dev libbz2-dev libicu-dev scons csh \
 enca ttf-anonymous-pro libperl4-corelibs-perl cgvg catfish gawk \
 i2c-tools sshfs wavesurfer audacity fcitx fcitx-chewing libswitch-perl bin86 \
 inotify-tools u-boot-tools subversion crash tree mscgen krename umbrello \
 intel2gas kernelshark trace-cmd pppoe dcfldd flex bison help2man \
 texinfo texi2html ghp-import autossh samba sdcv xournal cloc geogebra \
 libluajit-5.1-dev libacl1-dev libgpmg1-dev libgtk-3-dev libgtk2.0-dev \
 liblua5.2-dev libperl-dev libselinux1-dev libtinfo-dev libxaw7-dev \
 libxpm-dev libxt-dev gnome-control-center gettext libtool libtool-bin cmake g++ pkg-config unzip xsel

ubuntu 24.04 package

sudo apt-get install autoconf automake linux-headers-`uname -r` \
clang xdot git meld gparted cmake g++ pkg-config unzip xsel librust-openssl-dev \
terminator universal-ctags cscope htop libfuse2 ghp-import libpcre3-dev libpcre2-dev curl fonts-firacode \

How to make Balena Etcher work in Ubuntu 24.04?

sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

foliate 支持.epub,.mobi,.azw和.azw3文件。 它不支持PDF文件。

sudo add-apt-repository ppa:apandada1/foliate
sudo apt update
sudo apt install foliate

ubuntu 有線連線不見(網路圖示不見)解決方法

sudo apt-get install gnome-control-center
sudo service network-manager stop
sudo rm /var/lib/NetworkManager/NetworkManager.state
sudo service network-manager start
sudo gedit /etc/NetworkManager/NetworkManager.conf
(把false改成true)
sudo service network-manager restart

gnome-open

sudo ln -s /usr/bin/xdg-open ~/.mybin/o

將 /tmp 設到 RamDisk (tmpfs) 的方法

基本上只要打以下指令,就能將 /tmp 綁定到 /dev/shm
mkdir /dev/shm/tmp
chmod 1777 /dev/shm/tmp
sudo mount --bind /dev/shm/tmp /tmp

※ 註:為何是用 mount --bind 綁定,而不是 ln -s 軟連結,原因是 /tmp 目錄,系統不給刪除。

不過每次開機都要打指令才能用,這樣是行不通的,必須讓它開機時自動執行,才會方便。

用文書編輯器,建立 /etc/init.d/ramtmp.sh 內容如下:

#!/bin/sh
# RamDisk tmp
PATH=/sbin:/bin:/usr/bin:/usr/sbin

mkdir -p /dev/shm/tmp
mkdir -p /dev/shm/cache
mount --bind /dev/shm/tmp /tmp
mount --bind /dev/shm/cache /home/shihyu/.cache
chmod 1777 /dev/shm/tmp
chmod 1777 /dev/shm/cache


將此檔改權限為 755,使其可執行

sudo chmod 755 /etc/init.d/ramtmp.sh

在 /etc/rcS.d 中,建立相關軟連結(捷徑),使其一開機就執行 以下指令僅能終端機操作

cd /etc/rcS.d
sudo ln -s ../init.d/ramtmp.sh S50ramtmp.sh

im-config 新酷音

sudo apt-get install fcitx-table-boshiamy (嘸蝦米)
sudo apt-get install fcitx-table-cangjie-big (倉頡大字集)
sudo apt-get install fcitx-table-zhengma-large (鄭碼大字集)
sudo apt-get install fcitx-table-wubi-large (五筆大字集)
sudo apt-get install fcitx-chewing (新酷音)
sudo apt-get install fcitx-sunpinyin (雙拼)
sudo apt-get install fcitx-table-easy-big (輕鬆大詞庫)
sudo apt-get install fcitx-m17n
sudo apt-get remove ibus

im-config

選fcitx為預設
重開機或登入
在有可輸入中文的框中,按Ctrl+Space,然後用Ctrl+Shift選輸入法輸入,預設的簡繁轉換為Ctrl+Shift+F

Gitbook 安裝

sudo apt-get update
sudo apt-get install nodejs
sudo apt-get install npm

sudo npm install gitbook -g

node 安裝

https://nodejs.org/en/  

export N_PREFIX=$HOME/.mybin/node-v17.8.0-linux-x64/
export PATH=$N_PREFIX/bin:$PATH

多個 SSH Key 對應多個 Github 帳號

$ cd ~/.ssh
$ ssh-keygen -t rsa -C "account1@email.com" -f id_rsa_account1
$ ssh-keygen -t rsa -C "account2@email.com" -f id_rsa_account2

建立 config 檔
$ cd ~/.ssh
$ touch config
$ gedit config

#account1
Host github.com-account1
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_rsa_account1

#account2
Host github.com-account2
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_rsa_account2
    
修改相對應 repo 的 remote url。例如:
ssh://git@github.com-account1/github account/repo1.git
github account 是github帳號 ex : jasonblog , ccccjason , shihyu

Rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

更新 Rust 版本
rustup update

cmake Setup

wget https://cmake.org/files/v3.26/cmake-3.26.0.tar.gz
./bootstrap --prefix=$HOME/.mybin/cmake
make 
make install
export PATH="$HOME/.mybin/cmake/bin:$PATH"

新增虛擬記憶體(swap)

  1. 切割4GB空間做為swap使用。

    sudo fallocate -l 4G /swapfile
    

    4G:可視個人需求修改

    /swapfile:做為swap使用的檔案存放路徑,可修改

    這時你的硬碟空間就已被扣除4G保留為此檔案,做為虛擬記憶體使用。

    不過這時只是新增了一個容量4G大的檔案而已,系統並不知道要用他來做swap。

  2. 告知系統使用此檔案,啟用swap功能。

    sudo chmod 600 /swapfile
    sudo mkswap /swapfile
    sudo swapon /swapfile
    
  3. 將swap設定為開機自動掛載。

    sudo vim /etc/fstab
    …
    
    開啟檔案,到最後加入此行
    
    /swapfile   none swap    sw 0 0
    

    這樣以後每次重開機就會固定載入這個檔案做為虛擬記憶體使用了。

    附註:其實在虛擬網站伺服器都是使用SSD的現在,舉個例,如Linode的Linode 2GB規格,記憶體2G硬碟30G,設定個6G~10G來做虛擬記憶體,就等於你擁有8~12G的記憶體可能還太多了,如果主機有特殊需求使用大量記憶體,可以參考看看這個做法。

改時區

tzselect Please identify a location so that time zone rules can be set correctly. Please select a continent, ocean, “coord", or “TZ". \1) Africa \2) Americas \3) Antarctica \4) Asia \5) Atlantic Ocean \6) Australia \7) Europe \8) Indian Ocean \9) Pacific Ocean \10) coord – I want to use geographical coordinates. \11) TZ – I want to specify the time zone using the Posix TZ format. #? 4

Please select a country whose clocks agree with yours. \1) Afghanistan 18) Israel 35) Palestine \2) Armenia 19) Japan 36) Philippines \3) Azerbaijan 20) Jordan 37) Qatar \4) Bahrain 21) Kazakhstan 38) Russia \5) Bangladesh 22) Korea (North) 39) Saudi Arabia \6) Bhutan 23) Korea (South) 40) Singapore \7) Brunei 24) Kuwait 41) Sri Lanka \8) Cambodia 25) Kyrgyzstan 42) Syria \9) China 26) Laos 43) Taiwan \10) Cyprus 27) Lebanon 44) Tajikistan \11) East Timor 28) Macau 45) Thailand \12) Georgia 29) Malaysia 46) Turkmenistan \13) Hong Kong 30) Mongolia 47) United Arab Emirates \14) India 31) Myanmar (Burma) 48) Uzbekistan \15) Indonesia 32) Nepal 49) Vietnam \16) Iran 33) Oman 50) Yemen \17) Iraq 34) Pakistan #? 43

The following information has been given:

Taiwan

Therefore TZ=’Asia/Taipei’ will be used. Local time is now: Tue Apr 3 12:07:13 CST 2018. Universal Time is now: Tue Apr 3 04:07:13 UTC 2018. Is the above information OK? \1) Yes \2) No #? 1

cp /usr/share/zoneinfo/Asia/Taipei /etc/localtime

更新時間與測試

apt-get install ntpdate

ntpdate time.stdtime.gov.tw

hwclock -w

(將時間寫入 BIOS)

date

加入排程

vim /etc/crontab…(略)

SHELL=/bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# m h dom mon dow user command 10 5 * * * root ntpdate time.stdtime.gov.tw

crontab -e @daily /usr/sbin/ntpdate time.stdtime.gov.tw > /dev/null 將對時寫進 crontab 每天做一次

AdGuard VPN

Linux

怎麼安裝登入使用在 ubuntu

在 Ubuntu 上安裝、登入和使用 AdGuard VPN(特別是 AdGuard VPN CLI)非常簡單,以下是完整步驟:

1. 安裝 AdGuard VPN CLI

打開終端機(Terminal),輸入以下指令安裝最新穩定版本:

curl -fsSL https://raw.githubusercontent.com/AdguardTeam/AdGuardVPNCLI/master/scripts/release/install.sh | sh -s -- -v

系統會要求你輸入管理員密碼。安裝過程中會詢問是否在 /usr/local/bin 建立連結,按 y 確認即可[^1][^2][^3]。


2. 登入 AdGuard VPN

安裝完成後,輸入以下指令登入(或註冊)帳號:

adguardvpn-cli login

依照提示輸入你的電子郵件和密碼。若還沒有帳號,系統會引導你註冊[^4][^5][^2]。


3. 連線到 VPN

  • 快速連線(自動選擇最快或上次使用的伺服器):
adguardvpn-cli connect
  • 查看可用伺服器位置
adguardvpn-cli list-locations
  • 連線到指定位置(例如 Tokyo):
adguardvpn-cli connect -l Tokyo

或使用國家代碼(如 JP 或 US)[^4][^5][^2]。


4. 常用指令

  • 登出
adguardvpn-cli logout
  • 檢查更新
adguardvpn-cli check-update
  • 查看所有可用指令
adguardvpn-cli --help-all

5. 注意事項

  • AdGuard VPN CLI 沒有圖形介面,所有操作都在終端機進行[^2][^3]。
  • 建議定期檢查更新,確保安全性與穩定性[^4][^5]。

6. 移除 AdGuard VPN CLI

如需移除,請參考官方知識庫,通常可執行:

adguardvpn-cli uninstall

(若沒有此指令,請參考官方文件手動移除)


這樣你就可以在 Ubuntu 上順利安裝、登入及使用 AdGuard VPN CLI 了!

find . -type f -size +10M  ! -name '*.cpp' ! -name '*.c' ! -name '*.rs' ! -name '*.java'
find . -not -path '*/\.git/*' -not -path '*readme*' -type f    ! -name '*.cpp' ! -name '*.c' ! -name '*.rs' ! -name '*.java' ! -name '*.go' ! -name '*.cc' ! -name '*.h' ! -name '*.kt' ! -name '*.py'  ! -name '*.sh'  ! -name '*.asm' ! -name '*.pl' ! -name '*.sed'  ! -name '*.hpp'  ! -name '*.cxx' ! -name '*makefile*'  ! -name '*.json' -exec rm {} \;
  • https://github.com/dutchcoders/transfer.sh
transfer(){ if [ $# -eq 0 ];then echo "No arguments specified.\nUsage:\n transfer <file|directory>\n ... | transfer <file_name>">&2;return 1;fi;if tty -s;then file="$1";file_name=$(basename "$file");if [ ! -e "$file" ];then echo "$file: No such file or directory">&2;return 1;fi;if [ -d "$file" ];then file_name="$file_name.zip" ,;(cd "$file"&&zip -r -q - .)|curl --progress-bar --upload-file "-" "https://transfer.sh/$file_name"|tee /dev/null,;else cat "$file"|curl --progress-bar --upload-file "-" "https://transfer.sh/$file_name"|tee /dev/null;fi;else file_name=$1;curl --progress-bar --upload-file "-" "https://transfer.sh/$file_name"|tee /dev/null;fi;}
# Upload using cURL
$ curl --upload-file ./hello.txt https://transfer.sh/hello.txt https://transfer.sh/wPbvjO/hello.txt

# Using the shell function
$ transfer hello.txt
##################################################### 100.0% https://transfer.sh/zmlFh3/hello.txt

# Upload from web
Drag your files here, or click to browse.
find . -type f -not -path '*/\.git/*' -size +1M

GCP SSH 設定

好的,讓我整理完整的 GCP 新機器設定步驟:

  1. 生成 SSH 金鑰:
# 替換 your.email@gmail.com 為你的 Gmail
ssh-keygen -t rsa -b 2048 -C "your.email@gmail.com" -f ~/.ssh/myssh/gcp_new
gcp 帳號 chhi3758
ssh-keygen -t rsa -b 2048 -C "chhi3758" -f ~/.ssh/myssh/gcp_chhi3758
  1. 修改並複製公鑰:
# 查看公鑰
cat ~/.ssh/myssh/gcp_new.pub

# 格式應該要像:
ssh-rsa AAAAB3Nza... autoicash2023
# 注意:最後的使用者名稱要改成你的 GCP 登入帳號
  1. 在 GCP 主控台設定:
  • 前往:Compute Engine > 中繼資料 > SSH 金鑰
  • 點選「編輯」
  • 貼上修改後的公鑰
  1. 設定 SSH 金鑰權限:
chmod 600 ~/.ssh/myssh/gcp_new
ssh-add ~/.ssh/myssh/gcp_new
  1. SSH 連線:
ssh -i ~/.ssh/myssh/gcp_new autoicash2023@35.185.159.162
  1. 設定語系:
# 安裝語言包
sudo apt-get update
sudo apt-get install -y language-pack-zh-hant language-pack-zh-hans

# 設定 locales
sudo locale-gen zh_TW.UTF-8
sudo update-locale LANG=zh_TW.UTF-8 LC_ALL=zh_TW.UTF-8

# 編輯設定檔
sudo bash -c 'cat > /etc/default/locale << EOF
LANG=zh_TW.UTF-8
LANGUAGE=zh_TW:zh
LC_ALL=zh_TW.UTF-8
EOF'
  1. 設定時區:
# 安裝需要的套件
sudo apt-get install -y util-linux ntpdate

# 設定台北時區
sudo timedatectl set-timezone Asia/Taipei
# 或
sudo ln -sf /usr/share/zoneinfo/Asia/Taipei /etc/localtime

# 更新時間
sudo ntpdate time.stdtime.gov.tw

# 確認設定
date
timedatectl
  1. 重新登入以套用所有設定:
exit
# 然後重新 SSH 連線

重要提醒:

  • 確保公鑰內的使用者名稱與 SSH 連線時使用的相同
  • 每個指令執行後最好確認是否成功
  • 如果遇到問題,可以查看系統日誌:sudo tail -f /var/log/syslog

需要我解釋任何部分嗎?

Wine的編譯方法

下載Wine原始碼

git clone https://github.com/wine-mirror/wine

# 如果只是使用wine,就選擇下載不帶git的wine原始碼。
wget https://github.com/wine-mirror/wine/archive/refs/heads/master.zip && unzip master.zip && mv wine-master wine

# 如果要回溯wine或者維護補丁,就選擇下載帶git的wine原始碼。
# git clone git://source.winehq.org/git/wine.git ~/wine

安裝依賴

手動安裝依賴,以下是編譯依賴+64位運行依賴+32位運行依賴:

sudo apt install gcc-multilib g++-multilib flex bison gcc make gcc-mingw-w64 libasound2-dev libpulse-dev libdbus-1-dev libfontconfig1-dev libfreetype6-dev libgnutls28-dev  libpng-dev libtiff-dev libunwind-dev libx11-dev libxml2-dev libxslt1-dev libfaudio-dev libgstreamer1.0-dev libmpg123-dev libosmesa6-dev libudev-dev libvkd3d-dev libvulkan-dev libcapi20-dev liblcms2-dev libcups2-dev libgphoto2-dev libsane-dev libgsm1-dev libkrb5-dev libldap2-dev ocl-icd-opencl-dev libpcap-dev libusb-1.0-0-dev libv4l-dev libopenal-dev  libasound2-dev:i386 libpulse-dev:i386 libdbus-1-dev:i386 libfontconfig1-dev:i386 libfreetype6-dev:i386 libgnutls28-dev:i386 libpng-dev:i386 libtiff-dev:i386 libunwind-dev:i386 libx11-dev:i386 libxml2-dev:i386 libxslt1-dev:i386 libfaudio-dev:i386 libgstreamer1.0-dev:i386 libgstreamer-plugins-base1.0-dev:i386 libmpg123-dev:i386 libosmesa6-dev:i386 libsdl2-dev:i386 libudev-dev:i386 libvkd3d-dev:i386 libvulkan-dev:i386 libcapi20-dev:i386 liblcms2-dev:i386 libcups2-dev:i386 libgphoto2-dev:i386 libsane-dev:i386 libgsm1-dev:i386 libkrb5-dev:i386 libldap2-dev:i386 ocl-icd-opencl-dev:i386 libpcap-dev:i386 libusb-1.0-0-dev:i386 libv4l-dev:i386 libopenal-dev:i386 libjpeg-turbo8-dev libjpeg-turbo8-dev:i386 libxcomposite-dev libxcomposite-dev:i386 libc6-i386

編譯及運行wine

如果是使用wine,選擇安裝到系統目錄。
bash
# 編譯並安裝到系統目錄
cd wine 
./configure --disable-tests 
make -j$(nproc) 
sudo make install

# 使用wine運行內建應用notepad。
wine notepad
如果是測試wine,選擇安裝到指定目錄。
# 編譯並安裝到指定目錄
mkdir build release 
cd build 
../wine/configure --disable-tests  
make -j$(nproc) 
make install DESTDIR=../release

# 使用wine運行內建應用notepad。
../release/usr/local/bin/wine notepad

編譯64位wine

# 假設wine原始碼在src目錄中,然後在src同級建立開始執行
mkdir src win32 win64 release

cd win64
../src/configure --disable-tests --enable-win64
make -j$(nproc)
make install DESTDIR=../release

cd ../win32
../src/configure --disable-tests --with-wine64=../win64
make -j$(nproc)
make install DESTDIR=../release

製作自包含依賴

因為源會影響依賴包的安裝,所以有時需要製作離線的依賴包,用於編譯或運行。 以下方法需要在純淨環境中執行:

sudo apt clean

sudo apt install gcc-multilib g++-multilib  gcc make gcc-mingw-w64 \
flex bison libasound2-dev libpulse-dev libdbus-1-dev libfontconfig1-dev libfreetype6-dev libgnutls28-dev  libpng-dev libtiff-dev libunwind-dev libx11-dev libxml2-dev libxslt1-dev libfaudio-dev libgstreamer1.0-dev libmpg123-dev libosmesa6-dev libudev-dev libvkd3d-dev libvulkan-dev libcapi20-dev liblcms2-dev libcups2-dev libgphoto2-dev libsane-dev libgsm1-dev libkrb5-dev libldap2-dev ocl-icd-opencl-dev libpcap-dev libusb-1.0-0-dev libv4l-dev libopenal-dev  libasound2-dev:i386 libpulse-dev:i386 libdbus-1-dev:i386 libfontconfig1-dev:i386 libfreetype6-dev:i386 libgnutls28-dev:i386 libpng-dev:i386 libtiff-dev:i386 libunwind-dev:i386 libx11-dev:i386 libxml2-dev:i386 libxslt1-dev:i386 libfaudio-dev:i386 libgstreamer1.0-dev:i386 libgstreamer-plugins-base1.0-dev:i386 libmpg123-dev:i386 libosmesa6-dev:i386 libsdl2-dev:i386 libudev-dev:i386 libvkd3d-dev:i386 libvulkan-dev:i386 libcapi20-dev:i386 liblcms2-dev:i386 libcups2-dev:i386 libgphoto2-dev:i386 libsane-dev:i386 libgsm1-dev:i386 libkrb5-dev:i386 libldap2-dev:i386 ocl-icd-opencl-dev:i386 libpcap-dev:i386 libusb-1.0-0-dev:i386 libv4l-dev:i386 libopenal-dev:i386 libjpeg-turbo8-dev libjpeg-turbo8-dev:i386 libxcomposite-dev libxcomposite-dev:i386 libc6-i386

cp -r /var/cache/apt/archives ~
cd archives; rm -rf partial
mkdir ../wine-depends
x=$(ls);for y in $x;do dpkg-deb -x $y ../wine-depends;done
為了不影響系統環境,採用手動新增目錄方式來新增庫的搜尋路徑:

bash
export LD_LIBRARY_PATH=<搜尋路徑>:$LD_LIBRARY_PATH
https://android.googlesource.com/

https:*//android.googlesource.com/platform/manifest/
repo init -u https://android.googlesource.com/platform/manifest -b android-13.0.0_r18

repo sync -c -j8

安裝Android Sdk

sdk安裝方式

常規思路,下載sdk,安裝之後修改環境。但是發現,網路上已經沒有了sdk的下載資源,有的也只是很老的版本。查看Android開發文件——sdkmanager的使用指南,發現可以使用sdkmanager這個命令列工具進行下載。

下載sdkmanager工具包

官網下載頁最底部-命令列工具下載,找到Linux平臺的工具包

使用wget下載到伺服器

wget -P /home/android-sdk/ https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip

解壓工具包

unzip commandlinetools-linux-7583922_latest.zip

sdkmanager/home/android-sdk/cmdline-tools/bin下。

選擇下載最新的sdk版本

使用命令查看最新的stable版本

[root@192 bin]# ./sdkmanager --list --channel=0
Error: Could not determine SDK root.
Error: Either specify it explicitly with --sdk_root= or move this package into its expected location: <sdk>/cmdline-tools/latest/

報錯了,無法找到sdk根目錄,提示說有兩種解決辦法:一是用–sdk_root指定路徑,二是把資料夾移動到指定路徑。

因為懶,選擇試一下第二個一勞永逸的方法。

[root@192 cmdline-tools]# mkdir latest
[root@192 cmdline-tools]# mv bin/ lib/ NOTICE.txt source.properties -t latest/

再次執行查詢命令,就會查出一長條的版本。

[root@192 cmdline-tools]# cd latest/bin/
[root@192 bin]# ./sdkmanager --list --channel=0

安裝sdk

因為App項目使用了Android-30的版本,故安裝對應的platforms;android-30

./sdkmanager "build-tools;30.0.3" "platforms;android-30"
./sdkmanager "platform-tools" "build-tools;31.0.0" "build-tools;32.0.0" "platforms;android-31" "platforms;android-32"

在彈出協議許可時選擇y,就開始安裝了。

下載完成後,就可以在cmdline-tools的同級目錄,找到下載的sdk了。這也是為什麼上面要指定sdk-root的原因了。

[root@192 android-sdk]# ls
build-tools  cmdline-tools  commandlinetools-linux-7583922_latest.zip  emulator  licenses  patcher  platforms  platform-tools  tools

組態ANDROID_HOME環境變數

[root@192 android-sdk]# export ANDROID_HOME=/home/android-sdk
[root@192 android-sdk]# export PATH=$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$PATH
[root@192 android-sdk]# source /etc/profile

至此,Linux安裝Android Sdk完成!

Flutter

JDK(Java Development Kit)

wget https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gz

tar -xvf jdk-17_linux-x64_bin.tar.gz
sudo mv jdk-17.0.7 /usr/lib/jvm/

設置Java環境變量 為了讓系統能夠找到 JDK,需要設置 JAVA_HOME 環境變量,並將其添加到 PATH 變量中。可以通過vi ~/.bashrc來編輯 ~/.bashrc~/.profile 文件來實現。打開文件並在文件末尾添加以下內容:

export JAVA_HOME=/usr/lib/jvm/jdk-17.0.7
export PATH=$JAVA_HOME/bin:$PATH
dioxidecn@dioxidecn-virtual-machine:~$ java -version
java version "17.0.7" 2023-04-18 LTS
Java(TM) SE Runtime Environment (build 17.0.7+8-LTS-224)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.7+8-LTS-224, mixed mode, sharing)

在Ubuntu上進行Flutter開發,您需要安裝Android SDK。Flutter使用Android SDK來構建和運行Android應用程序。以下是安裝Android SDK的簡單步驟:

下載 Android SDK: 您可以從Android 開發者網站下載 Android SDK Command Line Tools。選擇壓縮檔案(ZIP)版本。

解壓縮檔案: 解壓縮下載的檔案到您選擇的目錄。例如,您可以將其解壓縮到/usr/local/android-sdk目錄下。

sudo mkdir /usr/local/android-sdk
sudo tar -xvzf commandlinetools-linux-xxxxxx_latest.zip -C /usr/local/android-sdk

設定環境變量: 在您的~/.bashrc~/.zshrc文件中添加以下環境變量:

export ANDROID_HOME="/media/shihyu/ssd/android-sdk"
export ANDROID_SDK_ROOT="/usr/local/android-sdk"
export PATH="$PATH:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools:$ANDROID_SDK_ROOT/tools/bin"

然後在終端機中運行source ~/.bashrcsource ~/.zshrc以應用變更。

安裝SDK組件: 運行以下命令來安裝所需的SDK組件:

sdkmanager --sdk_root=/media/shihyu/ssd/android-sdk --install "platforms;android-30" "build-tools;30.0.3" "platform-tools"

sdkmanager --sdk_root=/media/shihyu/ssd/android-sdk  --install "cmdline-tools;latest"

這將安裝Android 30平臺、30.0.3版本的構建工具以及平臺工具。

現在,您已經成功安裝了Android SDK,可以開始使用Flutter來開發Android應用程序。請注意,這些步驟可能需要根據您的系統配置和Android SDK版本進行微調。


在Ubuntu上安裝Flutter的最簡單步驟如下:

  1. 在終端機中運行以下命令,下載Flutter SDK:
git clone https://github.com/flutter/flutter.git
  1. 將Flutter的bin目錄添加到您的PATH環境變量中。您可以在~/.bashrc~/.zshrc文件中添加類似以下的行:
export PATH="$PATH:`pwd`/flutter/bin"
  1. 運行以下命令來檢查Flutter的依賴項:
flutter doctor --android-licenses
flutter doctor
  1. 根據flutter doctor的輸出,安裝所需的依賴項,例如Android Studio、Xcode(僅限Mac)、Dart等。
  2. 安裝完成後,重新啟動終端機,然後您應該能夠運行Flutter命令,例如:
flutter --version

現在,您已經在Ubuntu上成功安裝了Flutter,可以開始開發您的Flutter應用程序了。

Ubuntu 怎麼寫個 flutter 的 hello world apk

要在Ubuntu上創建一個Flutter的Hello World APK,請按照以下步驟進行:

  1. 安裝Flutter: 如果您尚未安裝Flutter,請參照前面的步驟進行安裝。

  2. 創建Flutter項目: 在終端機中運行以下命令來創建一個新的Flutter項目:

    flutter create hello_world
    

    這會在當前目錄中創建一個名為hello_world的Flutter項目。

  3. 進入項目目錄: 進入創建的項目目錄:

    cd hello_world
    
  4. 運行應用: 運行以下命令來確保一切正常:

    flutter run
    

    這將啟動您的應用程序。您可以在模擬器或連接的設備上看到Hello World應用程序運行。

  5. 生成APK: 當您確保應用程序運行正常後,您可以生成APK文件。在項目目錄中運行:

    flutter build apk
    

    這將在build/app/outputs/flutter-apk/目錄中生成一個APK文件,例如app-release.apk

現在,您已經成功生成了一個Flutter Hello World APK。您可以將這個APK安裝到Android設備上,或者在模擬器中運行。


Flutter 第一個介面

專案建立

# 使用 flutter cli 來建立
$flutter create --platforms ios,android -e trade_app
Signing iOS app for device deployment using developer identity: "Apple Development: Mao-Chin Hsu (XXXXXXXXXX)"
Creating project trade_app...
Resolving dependencies in trade_app...
Got dependencies in trade_app.
Wrote 73 files.

All done!
You can find general documentation for Flutter at: https://docs.flutter.dev/
Detailed API documentation is available at: https://api.flutter.dev/
If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev

In order to run your empty application, type:

  $ cd trade_app
  $ flutter run

Your empty application code is in trade_app/lib/main.dart.

這邊我先將專案命名為 trade_app 也要注意 Flutter 的專案名稱是遵循 Dart

use lowercase_with_underscores for package names.

Package names should be all lowercase, with underscores to separate words, just_like_this. Use only basic Latin letters and Arabic digits: [a-z0-9_]. Also, make sure the name is a valid Dart identifier – that it doesn’t start with digits and isn’t a reserved word.

Dart package 命名規則

因為我已經有訂閱 Apple Developer 所以會看到 Flutter 已經自動把專案內 iOS 的部分帶入我的開發者信息

專案結構

.
├── README.md
├── analysis_options.yaml
├── android
│   ├── app
│   ├── build.gradle
│   ├── gradle
│   ├── gradle.properties
│   ├── gradlew
│   ├── gradlew.bat
│   ├── local.properties
│   ├── settings.gradle
│   └── trade_app_android.iml
├── ios
│   ├── Flutter
│   ├── Runner
│   ├── Runner.xcodeproj
│   ├── Runner.xcworkspace
│   └── RunnerTests
├── lib
│   └── main.dart
├── pubspec.lock
├── pubspec.yaml
└── trade_app.iml

這邊說明一下剛剛下的創建命令 如果沒有下 --platforms ios,android 這樣專案內就不會出現 windows, linux, macOS 的資料夾 各位也可以視需求要不要加 我自己還是習慣 Flutter 是在行動裝置上的

至於 -e 就是不會出現範例 code,以及一大堆的註解說明 一樣,這看個人,我是覺得每次都要刪一大堆東西很煩

啟動模擬器

我這邊會先以 iOS 為主 畢竟 Flutter 就是以能夠同一份 Code 做跨平臺編譯出名

open -a Simulator

IMG

然後就可以在專案資料夾試著先跑起來 應該要是最基本的 Hello World!

IMG

tools

https://itsfoss.com/rust-cli-tools/

cargo install bottom procs zoxide du-dust exa tealdeer bat difftastic tokei hyperfine fd-find sshx --locked
curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | bash

~/.bashrc
eval "$(zoxide init bash)"
  • A very fast implementation of tldr in Rust: Simplified, example based and community-driven man pages.
tldr strace
  • bat
alias cat="bat --theme=\$(defaults read -globalDomain AppleInterfaceStyle &> /dev/null && echo default || echo GitHub)"
  • tig
https://github.com/jonas/tig
  • pandoc
pandoc -f markdown -t epub3 -o xxx.epub 1.md 2.md 3.m

最直覺的做法是每章自成一個 HTML 檔,作者原來也是這麼做。但起初 Pandoc 將所有 HTML 併成一個 ch001.xhtml (解壓 ePub 觀察到的),研究發現是因為作者把章標題放在 <h2>,<h1> 放書名,


而 Pandoc 的分檔是以 <h1> 區隔;故我寫了一段 PowerShell 將 h1 移除,h2 改 h1。h3 改 h2,h4 改 h3,修改後 ePub 也修正為一章一個 .html


。但有個已知問題,Pandoc 產生的目錄連結在 Google 電子書閱讀器套件或 Calibre 閱讀時功能正常,點各層章節都能跳到對映位置,但在我的 Kobo Forma 閱讀器上只能跳到章 (.html 層),無法跳到節 (.html#section-id)。
使用 ls -1 | cut -d " " -f 10- 命令來只列出檔案名稱,忽略 -rw-r--r-- 1 shihyu shihyu 8973 3月 13 20:06 這部分的檔案權限、所有者等詳細資訊,只顯示檔案名稱。 cut -d " " -f 10- 則是使用分割符號 " " (空格)分割每一行,選取第 10 個欄位到最後一個欄位,也就是檔案名稱。
  • sed
find . -type f -name "*" -exec sed -i  "s/hello/fuck/g" {} \;
  • 更新 git
#!/bin/bash

# Change to the directory containing the Git repositories
cd /path/to/directory

# Find all .git directories and run Git commands on each one
find . -name ".git" -type d -exec sh -c '
  cd "{}" && cd .. && 
  git stash && git pull
' \;
  • xelatex

    • XeLaTeX 簡簡單單讓 LaTeX 說中文
    %!TEX encoding = UTF-8 Unicode
    \usepackage{xeCJK}
    %\setCJKmainfont{標楷體}
    %\setCJKmainfont{LiHei Pro}
    \setCJKmainfont{PingFang TC}
    
  • 每個月底執行 command

0 0 28-31 * * [ "$(date +\%d -d tomorrow)" = "01" ] && command
  • 解壓 zip
 find . -type f -name '*.zip' -exec unzip -d unzip {} \;

asciinema 把終端操作錄製成 gif 動畫

asciinema 是一個開源工具,可以把終端上的操作錄制下來轉換成 git 動畫,也可以進一步使用 ffmpeg 將動畫圖片轉換成 mp4 視頻。

安裝

macOS

brew install asciinema

Shell

Ubuntu/Debian

sudo apt install asciinema

Shell

使用

錄制

asciinema rec demo.cast

Shell

使用 ctrl + d 或 exit 停止錄制

回放

asciinema play demo.cast

Shell

生成 gif 動畫

按照 asciinema 項目的使用建議,錄制好的配置文件可以上傳到官網,然後使用官方的腳本和連接嵌入到自己的網頁。如果需要離線使用,可以通過 asciinema2gif 生成動圖。

asciinema2gif 基於 nodejs 開發,配合 Docker 更簡單。

拉取鏡像

docker pull asciinema/asciicast2gif

Shell

製作 gif

docker run --rm -v $PWD:/data asciinema/asciicast2gif demo.cast demo.gif

Shell

注意 -v $PWD:/data 即宿主機與容器映射存儲的設置,$PWD 代表當前目錄,要確保 demo.cast 文件在當前路徑中。

轉換成 mp4

可以使用 ffmpeg 進一步將 gif 動圖轉換成 mp4 視頻:

ffmpeg -i demo.gif demo.mp4

製作個人專屬看盤軟體

https://medium.com/finmind/%E8%A3%BD%E4%BD%9C%E5%80%8B%E4%BA%BA%E5%B0%88%E5%B1%AC%E7%9C%8B%E7%9B%A4%E8%BB%9F%E9%AB%94-%E4%BA%8C-27081ce44689

首先,將以下專案 clone 下來

https://github.com/FinMind/finmind-visualization

(需要使用 docker 技術,請先確定你安裝了 docker)

1. 建立 docker network:

docker network create finmind_network

2. 建立相關的 services,包含 MySQL 資料庫、RabbitMQ、Redash:

docker-compose -f mysql.yml up -d
docker-compose -f rabbitmq.yml up -d
docker-compose -f redash.yml up -d

3. 建立 FinMind 工具,將使用 FinMind Data 進行視覺化:

如果你有 FinMind Api Token,使用以下指令

FINMIND_API_TOKEN=your_token docker-compose -f finmind.yml up -d

如果沒有 Token,使用以下指令(沒有 token,資料會更新比較慢)

docker-compose -f finmind.yml up -d

邏輯上是,拉 FinMind Data,做視覺化,也就是說,如果是會員,更新資料速度會比較快,如果非會員,資料也是會更新,只是比較慢一點。

4. 建立完成後,以下是相關 services 連線資訊

mysql: http://localhost:8080/
    user : finmind
    password : test
flower: http://localhost:5555/
rabbitmq: http://localhost:15672/
    user : worker
    password : worker
visualization: http://localhost:5000/
finmind-visualization-api: http://localhost:8888/docs

5. 建立 MySQL Table

進入以下連結,http://localhost:8888/docs,

img

img

成功後,就會建立相關 Table 了,可以到以下連結查看,http://localhost:8080,帳號密碼是,finmind/test。

img

6. 拉取歷史資料

目前 MySQL 資料庫都是空的,因此需要拉取 FinMind 資料,方式如下。

一樣進入以下連結,http://localhost:8888/docs,並根據下圖,選擇 dataset,以 TaiwanStockPrice 為例

img

7. 拉取資料方式

是透過 RabbitMQ 進行,可以使用以下連結進入,http://localhost:15672/,帳號密碼,worker/worker。

img

等待 queues 消化完成歸 0 後,跳轉到 MySQL,可以看到都已經成功拉取 TaiwanStockPrice 歷史資料了,如下

img

其他的 Data,也可以用一樣的方式更新歷史資料。

8. 如何定時更新資料?

這時會有讀者想問,如何定時更新資料,首先,跳轉到 scheduler 的 table

img

預設都是不要定時更新,這部分留給使用者自行決定。

9. 開始建立視覺化

前置工作都準備完成後,資料會定時更新了,開始進入主題,視覺化工具,進入以下連結,http://localhost:5000/,按照下圖步驟,帳號密碼讀者可自行設定

img

建立 Data Source,與 MySQL 做連結

img

連線參數

img

以上成功後,開始建立,第一個 Query

10. 建立第一個 Query

步驟如下圖

img

輸入以下 SQL

select
  *
from
  TaiwanStockPrice
where
  stock_id = '2330'
  and date >= '2022-01-01'

img

到這就完成了基本設定,下一篇文章將介紹,如何進行視覺化做圖,如何做出第一篇文章中的圖表。

img

為函數新增enter和exit級trace

日常開發中,我們為了輔助程序偵錯常常在每個函數的出入口(entry/exit)增加Trace,一般我們多用宏來實現這些Trace語句,例如:

#ifdef XX_DEBUG_
#define TRACE_ENTER() printf("Enter %s\n", __FUNCTION__)
#define TRACE_EXIT() printf("Exit %s\n", __FUNCTION__)
#else
#define TRACE_ENTER()
#define TRACE_EXIT()
#endif

// 有了TRACE_ENTER和TRACE_EXIT後,你就可以在你的函數中使用它們了。例如:
void foo(…)
{
    TRACE_ENTER();
    … …
    TRACE_EXIT();
}

這樣你就可以很容易看到函數的呼叫關係。不過這種手法用起來卻不輕鬆。首先你需要在每個函數中手工加入TRACE_ENTER和TRACE_EXIT,然後再利用XX_DEBUG_宏控制其是否生效。特別是對於初期未新增函數級Enter/Exit Trace的項目,後期加入工作量很大。

另外一種方便的手法:使用GCC的-finstrument-functions選項。-finstrument-functions使得GCC在生成程式碼時自動為每個函數在入口和出口生成__cyg_profile_func_enter和__cyg_profile_func_exit兩個函數呼叫。我們要做的就是給出一份兩個函數的實現即可。最簡單的實現莫過於列印出被呼叫函數的地址了:

/* func_trace.c */
__attribute__((no_instrument_function))
void __cyg_profile_func_enter(void* this_fn, void* call_site)
{
    printf("enter func => %p\n", this_fn);
}

__attribute__((no_instrument_function))
void __cyg_profile_func_exit(void* this_fn, void* call_site)
{
    printf("exit func <= %p\n", this_fn);
}

我們將這兩個函數放入libfunc_trace.so:

gcc -fPIC -shared -o libfunc_trace.so func_trace.c

我們為下面例子新增enter/exit級Trace:

#include <unistd.h>
/* example.c */
static void foo2()
{

}

void foo1()
{
    foo2();
}

void foo()
{
    chdir("/home/tonybai");
    foo1();
}

int main(int argc, const char* argv[])
{
    foo();
    return 0;
}
$ gcc -g example.c -o example -finstrument-functions -no-pie
$ LD_PRELOAD=libfunc_trace.so example
enter func => 0×8048524
enter func => 0x80484e5
enter func => 0x80484b2
enter func => 0×8048484
exit func <= 0×8048484
exit func <= 0x80484b2
exit func <= 0x80484e5
exit func <= 0×8048524

不過只輸出函數地址很難讓人滿意,根據這些地址我們無法得知到底對應的是哪個函數。那我們就嘗試一下將地址轉換為函數名後再輸出,這方面GNU依舊給我們提供了工具,它就是addr2line。addr2line是binutils包中的一個工具,它可以根據提供的地址在可執行檔案中找出對應的函數名、對應的原始碼檔案名稱以及行數。我們改造一下func_trace.c中的兩個函數的實現:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>

#define PATH_MAX 1024
#define gettid() syscall(SYS_gettid)
#define DUMP(func, call) printf("%s: func = %p, called by = %p/n", __FUNCTION__, func, call)

static char path[PATH_MAX];

__attribute__((constructor))
static void executable_path_init()
{
    char    buf[PATH_MAX];

    memset(buf, 0, sizeof(buf));
    memset(path, 0, sizeof(path));

#ifdef _SOLARIS_TRACE
    getcwd(buf, PATH_MAX);
    sprintf(path, "%s/%s", buf, getexecname());
#elif _LINUX_TRACE
    readlink("/proc/self/exe", path, PATH_MAX);
#else
#endif
}

#ifdef __cplusplus
extern "C" {
#endif
__attribute__((no_instrument_function))
void __cyg_profile_func_enter(void* this_fn, void* call_site)
{
    char buf[PATH_MAX];
    char cmd[PATH_MAX];

    memset(buf, 0, sizeof(buf));
    memset(cmd, 0, sizeof(cmd));

    sprintf(cmd, "addr2line %p -e %s -f|head -1", this_fn, path);
    printf("\n%s\n", cmd);

    FILE* ptr = NULL;
    memset(buf, 0, sizeof(buf));

    if ((ptr = popen(cmd, "r")) != NULL) {
        fgets(buf, PATH_MAX, ptr);
        printf("enter func => %p:%s", this_fn, buf);
    }

    (void) pclose(ptr);
}

__attribute__((no_instrument_function))
void __cyg_profile_func_exit(void* this_fn, void* call_site)
{
    char buf[PATH_MAX];
    char cmd[PATH_MAX];

    memset(buf, 0, sizeof(buf));
    memset(cmd, 0, sizeof(cmd));

    sprintf(cmd, "addr2line %p -e %s -f|head -1", this_fn, path);
    printf("\n%s\n", cmd);

    FILE* ptr = NULL;
    memset(buf, 0, sizeof(buf));

    if ((ptr = popen(cmd, "r")) != NULL) {
        fgets(buf, PATH_MAX, ptr);
        printf("exit func <= %p:%s", this_fn, buf);
    }

    (void) pclose(ptr);
}
#ifdef __cplusplus
}
#endif
gcc -D_LINUX_TRACE -fPIC -shared -o libfunc_trace.so func_trace.c
$ gcc -g example.c -o example -finstrument-functions -no-pie 
$ LD_PRELOAD=./libfunc_trace.so ./example
enter func => 0×8048524:main
enter func => 0x80484e5:foo
enter func => 0x80484b2:foo1
enter func => 0×8048484:foo2
exit func <= 0×8048484:foo2
exit func <= 0x80484b2:foo1
exit func <= 0x80484e5:foo
exit func <= 0×8048524:main

關於這個實現,還有幾點要說道說道: 首先libfunc_trace.so是動態連結到你的可執行程序中的,那麼如何獲取addr2line所需要的檔案名稱是一個問題;另外考慮到可執行程序中可能會呼叫chdir這樣的介面更換當前工作路徑,所以我們需要在初始化時就得到可執行檔案的絕對路徑供addr2line使用,否則會出現無法找到可執行檔案的錯誤。在這裡我們利用了GCC的__attribute__擴展: attribute((constructor))

這樣我們就可以在main之前就將可執行檔案的絕對路徑獲取到,並在__cyg_profile_func_enter和__cyg_profile_func_exit中直接引用這個路徑。

在不同平臺下獲取可執行檔案的絕對路徑的方法有不同,像Linux下可以利用"readlink /proc/self/exe"獲得可執行檔案的絕對路徑,而Solaris下則用getcwd和getexecname拼接。

再總結一下,如果你想使用上面的libfunc_trace.so,你需要做的事情有: 1、將編譯好的libfunc_trace.so放在某路徑下,並export LD_PRELOAD=PATH_TO_libfunc_trace.so/libfunc_trace.so 2、你的環境下需要安裝binutils的addr2line 3、你的應用在編譯時增加-finstrument_functions選項。

我已經將這個小工具包放到了Google Code上,有興趣的朋友可以在這裡下載完整原始碼包(20110715更新:支援輸出函數所在原始檔路徑以及所在行號,前提編譯你的程序時務必加上-g選項)

Cmake

常用的 CMake 參數:

  1. -DCMAKE_BUILD_TYPE: 用於指定編譯模式,例如 DebugRelease 等。
  2. -DCMAKE_INSTALL_PREFIX: 用於指定安裝路徑,預設為 /usr/local
  3. -G: 用於指定生成的專案文件類型,例如 Unix MakefilesVisual Studio 等。
  4. -DCMAKE_C_COMPILER-DCMAKE_CXX_COMPILER: 用於指定 C 和 C++ 編譯器的路徑。
  5. -D: 用於定義變數,例如 -DENABLE_FEATURE_A=ON
  6. -DBUILD_SHARED_LIBS: 用於指定是否生成共享庫(動態連結庫),默認為 OFF
  7. -DCMAKE_VERBOSE_MAKEFILE: 用於顯示詳細的編譯信息,包括編譯器命令和編譯時的選項。
  8. -DCMAKE_C_FLAGS-DCMAKE_CXX_FLAGS: 用於指定編譯器的選項,例如 -O2-Wall 等。
  9. -DCMAKE_MODULE_PATH: 用於指定 CMake 模組的路徑,例如 /usr/local/share/cmake/Modules
  10. -DCMAKE_PREFIX_PATH: 用於指定查找庫文件和頭文件的路徑,例如 /usr/local/lib/usr/local/include

以上是一些常用的 CMake 參數,你可以在需要的情況下使用它們來配置你的項目。


在 CMakeLists.txt 中,你可以使用一些指令和變數來配置你的 CMake 項目。以下是一些常用的指令和變數及其解釋:

指令:

  1. cmake_minimum_required(VERSION x.y): 指定 CMake 的最低版本需求,例如 cmake_minimum_required(VERSION 3.10)
  2. project(name [LANGUAGES lang1 lang2 ...]): 指定項目的名稱和支持的語言,例如 project(MyProject CXX)
  3. add_executable(name source1 [source2 ...]): 指定生成執行文件的名稱和源文件,例如 add_executable(MyProgram main.cpp)
  4. add_library(name type source1 [source2 ...]): 指定生成靜態庫或動態連結庫的名稱、類型和源文件,例如 add_library(MyLibrary STATIC lib.cpp)
  5. target_link_libraries(target library1 [library2 ...]): 指定目標文件需要連結的庫文件,例如 target_link_libraries(MyProgram MyLibrary)
  6. include_directories(dir1 [dir2 ...]): 指定頭文件搜索路徑,例如 include_directories(include)

變數:

  1. CMAKE_C_COMPILERCMAKE_CXX_COMPILER: 指定 C 和 C++ 編譯器的路徑,例如 set(CMAKE_CXX_COMPILER /usr/bin/g++)
  2. CMAKE_BUILD_TYPE: 指定編譯模式,例如 set(CMAKE_BUILD_TYPE Release)
  3. CMAKE_INSTALL_PREFIX: 指定安裝路徑,例如 set(CMAKE_INSTALL_PREFIX /usr/local)
  4. CMAKE_CXX_FLAGSCMAKE_C_FLAGS: 指定編譯器的選項,例如 set(CMAKE_CXX_FLAGS "-O2 -Wall")

以上是一些常用的 CMake 指令和變數,你可以在 CMakeLists.txt 文件中使用它們來配置你的項目。

set(CMAKE_BUILD_TYPE DEBUG)
set(CMAKE_C_FLAGS "-O0 -ggdb")
set(CMAKE_C_FLAGS_DEBUG "-O0 -ggdb")
set(CMAKE_C_FLAGS_RELEASE "-O0 -ggdb")
set(CMAKE_CXX_FLAGS "-O0 -ggdb")
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -ggdb")
set(CMAKE_CXX_FLAGS_RELEASE "-O0 -ggdb")

ufw:簡易防火牆設置

出處:https://noob.tw/ufw/

Linux 上的 iptables 可能太難,我們不是專業的資安工程師,也不是什麼 Linux 老鳥。像我們這種菜鳥,還是用 ufw 就好了。這篇整理一些常用的 ufw 設定。

安裝 ufw

ufw 的全名是 Uncomplicated Firewall,意思是不複雜的防火牆。它的指令不但好記,寫好的規則也淺顯易懂,不會像 iptables 的裹腳布又臭又長。

大部分的 Ubuntu 系統應該都已經裝好 ufw。如果你是 Debian,或是什麼特別瘦身版的 Ubuntu 的話,可以透過以下指令安裝:

sudo apt-get install ufw

設定防火牆預設規則

如果你想要規則嚴一點,可以預設封鎖所有通訊埠,再選擇性打開幾個 port;你也可以預設開放所有 port,然後再封鎖幾個 port。預設允許/封鎖的指令如下:

sudo ufw default allow # 預設允許
sudo ufw default deny # 預設封鎖

允許/封鎖通訊埠(port)

如果你要允許 SSH port 的話,可以這樣下:

sudo ufw allow ssh

或是

sudo ufw allow 22

也可以允許或封鎖其他的 port:

sudo ufw allow 80 # 允許 80
sudo ufw allow 443 # 允許 443
sudo ufw deny 3389 # 封鎖 3389
sudo ufw deny 21 # 封鎖 21

甚至可以一次允許一個範圍的 port:

sudo ufw allow 6000:6007/tcp # 允許 TCP 6000~6007
sudo ufw allow 6000:6007/udp # 允許 UDP 6000~6007

來自特定 IP 的規則

上面的規則是針對所有 IP,如果你想要針對某些 IP 可以不受控管,你也可以這樣設定:

sudo ufw allow from 192.168.11.10 # 允許 192.168.11.10 的所有連線
sudo ufw allow from 192.168.11.0/24 # 允許 192.168.11.1~192.168.11.255 的所有連線
sudo ufw deny from 192.168.11.4 # 封鎖 192.168.11.4 的所有連線

如果你只是不想讓某個小明偷偷連到你的 SSH Port,你也可以針對他封鎖:

sudo ufw deny from 192.168.11.7 to any port 22

查看目前設了什麼規則

推薦使用這個指令來看目前設了什麼規則:

sudo ufw status numbered

這個指令會幫你把規則前面加上編號:

Numbered Output:
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] 22                         ALLOW IN    Anywhere
[ 2] 80                         ALLOW IN    Anywhere
[ 3] 443                        ALLOW IN    Anywhere

如果你突然不喜歡某個規則了,可以直接刪除它:

sudo ufw delete 3

那個規則就不見囉!

開啟/關閉/重設防火牆

設定完所有規則後,記得把防火牆打開。

如果你是用 SSH 連線,別忘了要先 allow 自己的 SSH 連線。

sudo ufw enable # 啟用防火牆
sudo ufw disable # 停用防火牆

如果你把規則改爛了,想要重新來過的話,可以重設:

sudo ufw reset

Python 以 PyCryptodome 實作 AES 對稱式加密方法教學與範例

出處:

https://officeguide.cc/python-pycryptodome-aes-symmetric-encryption-tutorial-examples/

以下將介紹如何在 Python 中安裝並使用 PyCryptodome 模組,以 AES 加密方法對資料進行加密與解密。

安裝 PyCryptodome 模組

使用 pip 安裝 Python 的 PyCryptodome 模組:

# 安裝 PyCryptodome 模組
sudo pip3 install pycryptodome

這種安裝方式會讓 PyCryptodome 安裝在 Crypto 套件路徑之下,取代舊的 PyCrypto 模組,這兩個模組會互相干擾,所以不可以同時安裝。

測試 PyCryptodome 模組是否可以正常運作:

# 測試 PyCryptodome 模組
python3 -m Crypto.SelfTest

如果不希望影響到舊的 PyCrypto 模組,也可以選擇以獨立模組的方式安裝,將 PyCryptodome 模組安裝至 Cryptodome 套件路徑之下:

# 安裝 PyCryptodome 模組
sudo pip3 install pycryptodomex

# 測試 PyCryptodome 模組
python -m Cryptodome.SelfTest

產生隨機金鑰

AES 加密方式的區塊長度固定為 128 位元,而金鑰長度則可以是 128、192 或 256 位元,在用 AES 進行資料加密之前,亦須先建立一組金鑰,最簡單的方式就是以隨機的方式產生金鑰。

from Crypto.Random import get_random_bytes

# 產生 256 位元隨機金鑰(32 位元組 = 256 位元)
key = get_random_bytes(32)
print(key)
b'l\n\xe8\x7f#\xec{\xf9\x8a4\xb8hye\xe9V\\\xfb\x01\x08\x854\x89\xc9\xfc\x80\xa2S\x920@}'

以密碼產生金鑰

如果希望使用一般的密碼來對資料進行加密與解密,可以根據密碼與一串固定的 salt 字串,產生對應的金鑰。

首先以亂數方式產生一串隨機的資料作為固定的 salt:

# 產生 salt
print(get_random_bytes(32))
b'\xd0\x18\xa7QM\xd6\x9b\xebxu\xe4\xed\xa8\x83\xf6\xa3/\x01\x9c\x9e\x86n\xda;\x10EdD\xf7\x932\xcc'

為了方便起見,可以將這串 salt 直接寫在程式當中,搭配自己的密碼即可產生金鑰:

from Crypto.Protocol.KDF import PBKDF2

# 固定的 salt
salt = b'\xd0\x18\xa7QM\xd6\x9b\xebxu\xe4\xed\xa8\x83\xf6\xa3/\x01\x9c\x9e\x86n\xda;\x10EdD\xf7\x932\xcc'

# 密碼
password = 'my#password'

# 根據密碼與 salt 產生金鑰
key = PBKDF2(password, salt, dkLen=32)

儲存金鑰

實務上我們通常會將產生的金鑰儲存在檔案中,方便後續的加密與解密使用:

# 金鑰儲存位置
keyPath = "my_key.bin"

# 儲存金鑰
with open(keyPath, "wb") as f:
    f.write(key)

# 讀取金鑰
with open(keyPath, "rb") as f:
    keyFromFile = f.read()

# 檢查金鑰儲存
assert key == keyFromFile, '金鑰不符'

AES CBC 加密模式

以下是使用 AES 的 CBC 模式對資料進行加密的範例,以 CBC 模式加密時需要先對資料進行 padding 處理,再進行加密。

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

# 輸出的加密檔案名稱
outputFile = 'encrypted.bin'

# 要加密的資料(必須為 bytes)
data = b'My secret data.'

# 以金鑰搭配 CBC 模式建立 cipher 物件
cipher = AES.new(key, AES.MODE_CBC)

# 將輸入資料加上 padding 後進行加密
cipheredData = cipher.encrypt(pad(data, AES.block_size))

# 將初始向量與密文寫入檔案
with open(outputFile, "wb") as f:
    f.write(cipher.iv)
    f.write(cipheredData)

以下則是 CBC 模式的解密方式:

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

# 輸入的加密檔案名稱
inputFile = 'encrypted.bin'

# 從檔案讀取初始向量與密文
with open(inputFile, "rb") as f:
    iv = f.read(16)         # 讀取 16 位元組的初始向量
    cipheredData = f.read() # 讀取其餘的密文

# 以金鑰搭配 CBC 模式與初始向量建立 cipher 物件
cipher = AES.new(key, AES.MODE_CBC, iv=iv)

# 解密後進行 unpadding
originalData = unpad(cipher.decrypt(cipheredData), AES.block_size)

# 輸出解密後的資料
print(originalData)
b'My secret data.'

AES CFB 加密模式

AES 的 CFB 模式跟 CBC 模式很類似,不過資料在加密之前不需要經過 padding 處理。

from Crypto.Cipher import AES

# 輸出的加密檔案名稱
outputFile = 'encrypted.bin'

# 要加密的資料(必須為 bytes)
data = b'My secret data.'

# 以金鑰搭配 CFB 模式建立 cipher 物件
cipher = AES.new(key, AES.MODE_CFB)

# 將輸入資料進行加密
cipheredData = cipher.encrypt(data)

# 將初始向量與密文寫入檔案
with open(outputFile, "wb") as f:
    f.write(cipher.iv)
    f.write(cipheredData)

以下則是 CFB 模式的解密方式:

from Crypto.Cipher import AES

# 輸入的加密檔案名稱
inputFile = 'encrypted.bin'

# 從檔案讀取初始向量與密文
with open(inputFile, "rb") as f:
    iv = f.read(16)         # 讀取 16 位元組的初始向量
    cipheredData = f.read() # 讀取其餘的密文

# 以金鑰搭配 CFB 模式與初始向量建立 cipher 物件
cipher = AES.new(key, AES.MODE_CFB, iv=iv)

# 解密資料
originalData = cipher.decrypt(cipheredData)

# 輸出解密後的資料
print(originalData)
b'My secret data.'

AES EAX 加密模式

AES 的 EAX 加密模式會產生 nonce 與 tag,這兩項必須連同密文一起儲存起來。

from Crypto.Cipher import AES

# 輸出的加密檔案名稱
outputFile = 'encrypted.bin'

# 要加密的資料(必須為 bytes)
data = b'My secret data.'

# 以金鑰搭配 EAX 模式建立 cipher 物件
cipher = AES.new(key, AES.MODE_EAX)

# 將輸入資料進行加密
cipheredData, tag = cipher.encrypt_and_digest(data)

# 將 nonce、tag 與密文寫入檔案
with open(outputFile, "wb") as f:
    f.write(cipher.nonce)
    f.write(tag)
    f.write(cipheredData)

以下則是 EAX 模式的解密方式:

from Crypto.Cipher import AES

# 輸入的加密檔案名稱
inputFile = 'encrypted.bin'

# 從檔案讀取初始向量與密文
with open(inputFile, "rb") as f:
    nonce = f.read(16)      # 讀取 16 位元組的 nonce
    tag = f.read(16)        # 讀取 16 位元組的 tag
    cipheredData = f.read() # 讀取其餘的密文

# 以金鑰搭配 EAX 模式與 nonce 建立 cipher 物件
cipher = AES.new(key, AES.MODE_EAX, nonce)

# 解密並驗證資料
originalData = cipher.decrypt_and_verify(cipheredData, tag)

# 輸出解密後的資料
print(originalData)
b'My secret data.'

Base64 編碼與 JSON 儲存格式

如果要將密文等資料儲存至資料庫或是進行網路傳輸,可以考慮將資料經過 base64 編碼之後,放在一個 JSON 檔案中,以下是一個簡單的範例。

import json
from base64 import b64encode, b64decode

# 要儲存的原始資料
ciphertext = b'...'
iv = b'...'

# 建立字典結構
outputJSON = {
    'ciphertext': b64encode(ciphertext).decode('utf-8'),
    'iv': b64encode(iv).decode('utf-8')
}

# 儲存為 JSON 檔案
with open('encrypted.json', 'w') as f:
    json.dump(outputJSON, f)

# 讀取 JSON 檔案
with open('encrypted.json') as f:
    inputJSON = json.load(f)

# 取用資料
ciphertext = b64decode(inputJSON['ciphertext'].encode('utf-8'))
iv = b64decode(inputJSON['iv'].encode('utf-8'))

參考資料:PyCryptodomeNitratineNitratine


from base64 import b64encode, b64decode
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
import json


class AESCrypt:
    # 固定的 salt
    salt = b"\xd0\x18\xa7QM\xd6\x9b\xebxu\xe4\xed\xa8\x83\xf6\xa3/\x01\x9c\x9e\x86n\xda;\x10EdD\xf7\x932\xcc"

    def __init__(self, password):
        """
        建立 AESCrypt 實例
        """
        self.password = password
        # 根據密碼與 salt 產生金鑰
        self.key = self.generate_key()

    def generate_key(self):
        """
        根據密碼與 salt 生成 PBKDF2 金鑰
        """
        key = PBKDF2(self.password, AESCrypt.salt, 32)
        return key

    def encrypt(self, data):
        """
        使用 AES 加密資料
        """
        # 建立加密器
        cipher = AES.new(self.key, AES.MODE_CTR)

        # 將資料加密
        ciphertext = cipher.encrypt(data)

        # 回傳加密後的資料與 nonce
        return (b64encode(ciphertext), b64encode(cipher.nonce))

    def decrypt(self, data):
        """
        使用 AES 解密資料
        """
        # 將傳入的資料解析為密文與 nonce
        ciphertext, nonce = b64decode(data[0]), b64decode(data[1])

        # 建立解密器
        cipher = AES.new(self.key, AES.MODE_CTR, nonce=nonce)

        # 解密資料並回傳
        return cipher.decrypt(ciphertext)

if __name__ == "__main__":
    # 創建 AESCrypt 實例
    aes = AESCrypt("my#password")

    # 需要加密的資料
    data = {"id": 12345, "name": "Alice", "age": 30}

    # 編碼為 JSON 格式
    data_json = json.dumps(data)

    # 加密
    encrypted_data = aes.encrypt(data_json.encode("utf-8"))

    # 將加密後的資料寫入文件
    with open("data.dat", "wb") as f:
        f.write(encrypted_data[0])
        f.write(b"\n")
        f.write(encrypted_data[1])

    # 從文件中讀取加密資料
    with open("data.dat", "rb") as f:
        encrypted_data = (f.readline().strip(), f.readline().strip())

    # 解密
    decrypted_data = aes.decrypt(encrypted_data)

    # 將解密後的資料反序列化為 Python 對象
    data_decrypt = json.loads(decrypted_data.decode("utf-8"))
    print(data_decrypt, type(data_decrypt))

lightweight-charts

npm install lightweight-charts svelte-lightweight-charts
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>My Chart</title>
    <script src="https://unpkg.com/lightweight-charts@3.0.0/dist/lightweight-charts.standalone.production.js"></script>
  </head>
  <body>
    <div id="chart"></div>
    <script>
      const symbol = 'BTCUSDT';
      const interval = '1d';
      const limit = 1000;

      const url = `https://api.binance.com/api/v3/klines?symbol=${symbol}&interval=${interval}&limit=${limit}`;

      fetch(url)
        .then(response => response.json())
        .then(data => {
          const chartData = data.map(item => ({
            time: item[0] / 1000,
            open: parseFloat(item[1]),
            high: parseFloat(item[2]),
            low: parseFloat(item[3]),
            close: parseFloat(item[4]),
          }));

          const chart = LightweightCharts.createChart(document.getElementById('chart'), {
            width: 600,
            height: 300,
          });

          const candlestickSeries = chart.addCandlestickSeries();

          candlestickSeries.setData(chartData);
        });
    </script>
  </body>
</html>

trading-vue-js

https://github.com/tvjsx/trading-vue-js

npm i trading-vue-js

Reference

  • https://medium.com/marcius-studio/financial-charts-for-your-application-cfcceb147786
  • https://codesandbox.io/examples/package/trading-vue-js

Vue

Sure, I'll provide you with a step-by-step guide assuming you're using a standard Vue CLI setup. If you don't have Vue CLI installed, you can install it globally using:

npm install -g @vue/cli

Now, let's assume you have a Vue CLI project set up or you want to create a new one. Follow these steps:

  1. Create a new Vue CLI project:

    vue create my-vue-trading-app
    

    Replace my-vue-trading-app with the desired name for your project. Follow the prompts to set up your project.

  2. Navigate to your project folder:

    cd my-vue-trading-app
    
  3. Install vue-tradingview-widgets:

    npm install vue-tradingview-widgets
    
  4. Replace the content of src/App.vue with your template and script:

    <template>
      <div id="app">
        <Chart />
        <CryptoMarket />
        <Snaps />
        <Screener />
      </div>
    </template>
    
    <script>
    import { defineComponent } from 'vue';
    import { Chart, CryptoMarket, Snaps, Screener } from 'vue-tradingview-widgets';
    
    export default defineComponent({
      name: 'App',
      components: {
        Chart,
        CryptoMarket,
        Screener,
        Snaps,
      },
    });
    </script>
    
    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    </style>
    
    <template>
      <div id="app">
        <Chart />
      </div>
    </template>
    
    <script>
    import { defineComponent } from 'vue';
    import { Chart } from 'vue-tradingview-widgets';
    
    export default defineComponent({
      name: 'App',
      components: {
        Chart,
      },
    });
    </script>
    
    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    </style>
    
  5. Run your Vue.js application:

    npm run serve
    

Network

npm run serve -- --port 8081
sudo ufw allow 8081
sudo lsof -i :8081

VIM

NVIM 編譯

# 關閉  anaconda 因為編譯環境會導向 anaconda lib
conda deactivate  

sudo apt-get install ninja-build \
     gettext libtool libtool-bin \
     autoconf automake cmake g++ \
     pkg-config unzip xsel

git clone https://github.com/neovim/neovim.git
cd neovim
git checkout stable
make CMAKE_EXTRA_FLAGS="-DCMAKE_INSTALL_PREFIX=$HOME/.mybin/nvim" CMAKE_BUILD_TYPE=Release  -j8
make install

cd ~/.mybin/nvim/share/nvim/runtime/autoload
wget https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

# copy vimrc to /home/shihyu/.config/nvim/init.vim   
 /home/shihyu/.config/nvim/init.vim
 
 
No "python3" provider found. Run :checkhealth provider
pip install --user --upgrade pynvim


# node 要新版的不然 coc 會報錯
sudo n 16 

// Enable Copy and Paste 
sudo apt-get install xclip xsel
CocInstall coc-rust-analyzer coc-tabnine coc-clangd coc-cmake coc-css coc-html coc-json coc-r-lsp coc-go coc-pyright coc-tsserver coc-sh

Inlay Hints默認是打開的,下次打開vim還會啟用,永久關閉可以在coc-nvim的組態檔案裡修改。 vim裡執行:CocConfig打開coc的組態檔案,新增:

{
    "inlayHint.enable":false
}
  • ~/.config/nvim/coc-settings.json
{
// 要空
}
clangd.path 要空的
CocInstall coc-clangd
CocCommand clangd.install

CocInstall coc-go
CocCommand go.install.gopls

CocInstall coc-pyright
CocInstall coc-json coc-tsserver
# neovim
## Config (`.config/nvim/init.vim`)
```
" Neovim vimrc 

"filetype plugin indent on    " required

" Use Vim settings rather than Vi settings
set nocompatible

" Make backspace normal
set backspace=indent,eol,start

" Syntax highlighting
syntax on
filetype plugin on

" Show line numbers
set number

" Allow hidden buffers, don't limit 1 file per window/split
set hidden

" Set default Vim terminal window size to account for line number spacing
" set lines=24 columns=82

" Swaps, Undo, Backups
set undodir=~/.vim/undo/
set backupdir=~/.vim/backups/
set directory=~/.vim/swaps/
" Added 12/14/2017
set backup
set undofile
set swapfile
" Source .vimrc if present in working directory
set exrc

" Restrict usage of write/execution shell commands
set secure

" Search down into subfolders and provide tab-completion for file-tasks
set path+=**
set tabstop=4
set shiftwidth=4
set expandtab
" Attempt to fix tmux/gnome-terminal color differences in Vim
set background=dark

" Set default color 
colorscheme slate

" Alter line numbers and background
hi Normal ctermbg=none
hi LineNr ctermfg=blue

" Add a color column at max line len
hi ColorColumn ctermbg=darkgrey

" Try and fix netrw:
" Make smaller and rid banner
" from here: https://shapeshed.com/vim-netrw/
let g:netrw_banner = 0
let g:netrw_winsize = 25

let g:vim_jsx_pretty_colorful_config = 1 " default 0
" python path
"
let g:python3_host_prog="/usr/bin/python3"
" Set neovim's clipboard 
" https://github.com/neovim/neovim/wiki/FAQ#how-to-use-the-windows-clipboard-from-wsl
set clipboard=unnamedplus

call plug#begin()
Plug 'neoclide/coc.nvim', {'branch': 'release'}
Plug 'HerringtonDarkholme/yats.vim'
Plug 'yuezk/vim-js'
Plug 'maxmellon/vim-jsx-pretty'
if has('nvim')
  Plug 'Shougo/denite.nvim', { 'do': ':UpdateRemotePlugins' }
else
  Plug 'Shougo/denite.nvim'
  Plug 'roxma/nvim-yarp'
  Plug 'roxma/vim-hug-neovim-rpc'
endif
call plug#end()

" === Denite setup ==="
" Use ripgrep for searching current directory for files
" By default, ripgrep will respect rules in .gitignore
"   --files: Print each file that would be searched (but don't search)
"   --glob:  Include or exclues files for searching that match the given glob
"            (aka ignore .git files)
"
call denite#custom#var('file/rec', 'command', ['rg', '--files', '--glob', '!.git'])

" Use ripgrep in place of "grep"
call denite#custom#var('grep', 'command', ['rg'])

" Custom options for ripgrep
"   --vimgrep:  Show results with every match on it's own line
"   --hidden:   Search hidden directories and files
"   --heading:  Show the file name above clusters of matches from each file
"   --S:        Search case insensitively if the pattern is all lowercase
call denite#custom#var('grep', 'default_opts', ['--hidden', '--vimgrep', '--heading', '-S'])

" Recommended defaults for ripgrep via Denite docs
call denite#custom#var('grep', 'recursive_opts', [])
call denite#custom#var('grep', 'pattern_opt', ['--regexp'])
call denite#custom#var('grep', 'separator', ['--'])
call denite#custom#var('grep', 'final_opts', [])

" Remove date from buffer list
call denite#custom#var('buffer', 'date_format', '')

" Custom options for Denite
"   auto_resize             - Auto resize the Denite window height automatically.
"   prompt                  - Customize denite prompt
"   direction               - Specify Denite window direction as directly below current pane
"   winminheight            - Specify min height for Denite window
"   highlight_mode_insert   - Specify h1-CursorLine in insert mode
"   prompt_highlight        - Specify color of prompt
"   highlight_matched_char  - Matched characters highlight
"   highlight_matched_range - matched range highlight
let s:denite_options = {'default' : {
\ 'split': 'floating',
\ 'start_filter': 1,
\ 'auto_resize': 1,
\ 'source_names': 'short',
\ 'prompt': 'λ ',
\ 'highlight_matched_char': 'QuickFixLine',
\ 'highlight_matched_range': 'Visual',
\ 'highlight_window_background': 'Visual',
\ 'highlight_filter_background': 'DiffAdd',
\ 'winrow': 1,
\ 'vertical_preview': 1
\ }}

" Loop through denite options and enable them
function! s:profile(opts) abort
  for l:fname in keys(a:opts)
    for l:dopt in keys(a:opts[l:fname])
      call denite#custom#option(l:fname, l:dopt, a:opts[l:fname][l:dopt])
    endfor
  endfor
endfunction

call s:profile(s:denite_options)

" Special filetype settings
autocmd FileType html setlocal shiftwidth=2 tabstop=2
autocmd FileType javascript setlocal shiftwidth=2 tabstop=2
autocmd FileType css setlocal shiftwidth=2 tabstop=2
autocmd FileType jsx setlocal shiftwidth=2 tabstop=2
autocmd FileType tsx setlocal shiftwidth=2 tabstop=2
autocmd FileType ts setlocal shiftwidth=2 tabstop=2
autocmd FileType py setlocal shiftwidth=4 tabstop=4
```
## Setup
### Plugin system [`vim-plug`](https://github.com/junegunn/vim-plug)
```
sh -c 'curl -fLo "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/autoload/plug.vim --create-dirs \
       https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'
```
### Language servers
- `JavaScript`: https://github.com/neoclide/coc-tsserver `:CocInstall coc-tsserver`

- `Python`: https://github.com/neoclide/coc.nvim/wiki/Language-servers#python `:CocInstall coc-python`

- `bash`: https://github.com/josa42/coc-sh `:CocInstall coc-sh`

- `C/C++`: 
RHEL8: 
```
sudo dnf install clang
```

Ubuntu:
```
sudo apt install clangd-10 # as of fall 2020
```
In vim/neovim:
```
:CocInstall coc-clangd
```
- `cmake`: `:CocInstall coc-cmake`
- `css`: `:CocInstall coc-css`
- `html`: `:CocInstall coc-html`
- `json`: `:CocInstall coc-json`
- `R`: `:CocInstall coc-r-lsp`
- `go`: `:CocInstall coc-go`
- `python`: `:CocInstall coc-pyright`

Do a bunch, all in one go, e.g. `:CocInstall coc-tabnine coc-clangd coc-cmake coc-css coc-html coc-json coc-r-lsp coc-go coc-pyright coc-tsserver coc-sh coc-rls`!

### Tabnine Pro Key
If you're a user of [TabNine](https:://www.TabNine.com) within multiple editing environments, you probably have a pro key. As [coc-tabnine](https://github.com/neoclide/coc-tabnine) is community supported, you'll need to use the `TabNine::config` **Magic String** to configure your key in settings.

copilot

https://github.com/github/copilot.vim

  1. Install Node.js 12 or newer.

  2. Install Neovim 0.6 or newer.

  3. Install github/copilot.vim using vim-plug, packer.nvim, or any other plugin manager. Or to install directly:

    • 手動更新
    git clone https://github.com/github/copilot.vim.git \
      ~/.config/nvim/pack/github/start/copilot.vim
    
  4. Start Neovim and invoke :Copilot setup.

https://github.com/github/copilot.vim/blob/release/doc/copilot.txt

:Copilot disable        Globally disable GitHub Copilot inline suggestions.

                                                *:Copilot_enable*
:Copilot enable         Re-enable GitHub Copilot after :Copilot disable.

                                                *:Copilot_feedback*
:Copilot feedback       Open the website for providing GitHub Copilot
                        feedback.

                                                *:Copilot_setup*
:Copilot setup          Authenticate and enable GitHub Copilot.

                                                *:Copilot_signout*
:Copilot signout        Sign out of GitHub Copilot.

                                                *:Copilot_status*
:Copilot status         Check if GitHub Copilot is operational for the current
                        buffer and report on any issues.

                                                *:Copilot_panel*
:Copilot panel          Open a window with up to 10 completions for the
                        current buffer.  Use <CR> to accept a solution.  Maps
                        are also provided for [[ and ]] to jump from solution
                        to solution.  This is the default command if :Copilot
                        is called without an argument.

coc-tabnine

vimを起動した狀態で以下のコマンドを実行します。

:CocInstall coc-tabnine

依存しているTabnineをコマンドで追加インストールします。

:CocCommand tabnine.updateTabNine

以下の表示が出たら完了です。Restartしておきましょう。

:CocRestart

GDB

sudo apt-get install libgmp-dev libmpfr-dev

git clone https://github.com/bminor/binutils-gdb

../configure --enable-targets=all \
--prefix=/home/shihyu/.mybin/gdb

make -j8 && make

編譯 gdb-7.9 build gdbserver and gdb

建議: 給特定用戶安裝 GDB 的 pretty-printer 打印出可讀性更好的 stdc++ 的 STL 容器 在編譯 GDB 之前,先安裝 ncurses 庫和 Python 庫(用於在 GDB 中 開啟 Python 支持,編譯 GDB 時必須添加 --with-python 選項)。

sudo apt-get install texinfo libncurses-dev libreadline-dev python-dev
gdb 編譯新版修改  // 因為 gdb 會出現  'g' packet reply is too long:
修改gdb/remote.c文件,屏蔽process_g_packet函數中的下列兩行:
if (buf_len > 2 * rsa->sizeof_g_packet)
error (_(“Remote ‘g’ packet reply is too long: %s”), rs->buf);
在其後添加:
if (buf_len > 2 * rsa->sizeof_g_packet) {
    rsa->sizeof_g_packet = buf_len ;
    for (i = 0; i < gdbarch_num_regs (gdbarch); i++)
    {
        if (rsa->regs[i].pnum == -1)
            continue;

        if (rsa->regs[i].offset >= rsa->sizeof_g_packet)
            rsa->regs[i].in_g_packet = 0;
        else
            rsa->regs[i].in_g_packet = 1;
    }
}

找到python可執行程序的位置

which python

/home/shihyu/anaconda2/bin/python

若為 Anaconda,則使用 Anaconda/lib

設置環境變量

export LDFLAGS="-Wl,-rpath,/home/shihyu/anaconda3/lib -L/home/shihyu/anaconda3/lib"

../configure --enable-targets=all \
             --enable-64-bit-bfd  \
             --with-python=python3.7 \
             --with-system-readline \
             --prefix=/home/shihyu/.mybin/gdb8.3_python3
設置環境變量

export LDFLAGS="-Wl,-rpath,/home/shihyu/anaconda2/lib -L/home/shihyu/anaconda2/lib"

./configure --enable-targets=all \
            --enable-64-bit-bfd \
            --with-python="/home/shihyu/anaconda2/bin/" \
            --with-system-readline \
            --prefix=/home/shihyu/.mybin/gdb_8.1
mkdir build ; cd build

export LDFLAGS="-Wl,-rpath,/home/shihyu/anaconda3/lib -L/home/shihyu/anaconda3/lib"

../configure --enable-targets=all \
             --enable-64-bit-bfd  \
             --with-python=python3.6 \
             --with-system-readline \
             --prefix=/home/shihyu/.mybin/gdb_python3
../configure --enable-targets=all \
             --enable-64-bit-bfd  \
             --with-python=python3 \
             --with-system-readline \
             --prefix=/home/shihyu/.mybin/gdb_python3
./configure --enable-targets=all \
            --enable-64-bit-bfd \
            --with-python \
            --with-system-readline \
            --prefix=/home/shihyu/.mybin/gdb_8.1




# --target=arm-linux表示生成的gdb調試的目標是在arm核心Linux系統中運行的程序
# --enable-targets=all  gdb可以用同一個版本支持x86,ppc等多種體系結構。
# 比較新的bfd中,當設置的target是64位或者打開--enable-targets=all的時候,不需要設置會自動打開這個選項,不過保險起見還是打開。這樣編譯出的GDB就能支持GDB支持的全部體系結構了。
make
sudo make install

把[GCC源碼目錄]/libstdc++-v3/python 複製到任意一個目錄(比如 ~/.mybin/gdb_8.1 目錄下), 如果源碼目錄下沒有上述 python 目錄,也可以用如下方式從遠程庫拉取之後再放到 ~/.mybin/gdb_8.1 目錄下:

svn co svn://gcc.gnu.org/svn/gcc/trunk/libstdc++-v3/python

然後,編輯 ~/.gdbinit,添加如下內容

python
import sys
import os
p = os.path.expanduser('~/.gdb/python') 
print p
if os.path.exists(p):
    sys.path.insert(0, p)
    from libstdcxx.v6.printers import register_libstdcxx_printers
    register_libstdcxx_printers(None)
end
(gdb) set architecture
Display all 204 possibilities? (y or n)
alpha                          m68k:isa-c:nodiv:mac
alpha:ev4                      m88k:88100
alpha:ev5                      mep
alpha:ev6                      mips
am33                           mips:10000
am33-2                         mips:12000
arm                            mips:16
armv2                          mips:3000
armv2a                         mips:3900
armv3                          mips:4000
armv3m                         mips:4010
armv4                          mips:4100
armv4t                         mips:4111
armv5                          mips:4120
armv5t                         mips:4300
armv5te                        mips:4400
auto                           mips:4600
avr                            mips:4650
avr:1                          mips:5000
avr:2                          mips:5400
avr:3                          mips:5500
avr:4                          mips:6000
avr:5                          mips:7000
avr:6                          mips:8000
cris                           mips:9000
cris:common_v10_v32            mips:isa32
crisv32                        mips:isa32r2
ep9312                         mips:isa64
fr300                          mips:isa64r2
fr400                          mips:loongson_2e
fr450                          mips:loongson_2f
fr500                          mips:mips5
fr550                          mips:octeon
frv                            mips:sb1
h1                             mn10300
h8300                          ms1
h8300h                         ms1-003
h8300hn                        ms2
h8300s                         powerpc:403
h8300sn                        powerpc:601
h8300sx                        powerpc:603
h8300sxn                       powerpc:604
hppa1.0                        powerpc:620
i386                           powerpc:630
i386:intel                     powerpc:7400
i386:x86-64                    powerpc:750
i386:x86-64:intel              powerpc:EC603e
i8086                          powerpc:MPC8XX
ia64-elf32                     powerpc:a35
ia64-elf64                     powerpc:common
iq10                           powerpc:common64
iq2000                         powerpc:e500
iwmmxt                         powerpc:rs64ii
iwmmxt2                        powerpc:rs64iii
m16c                           rs6000:6000
m32c                           rs6000:rs1
m32r                           rs6000:rs2
m32r2                          rs6000:rsc
m32rx                          s390:31-bit
m68hc11                        s390:64-bit
m68hc12                        score
m68k                           sh
m68k:5200                      sh-dsp
m68k:5206e                     sh2
m68k:521x                      sh2a
m68k:5249                      sh2a-nofpu
m68k:528x                      sh2a-nofpu-or-sh3-nommu
m68k:5307                      sh2a-nofpu-or-sh4-nommu-nofpu
m68k:5407                      sh2a-or-sh3e
m68k:547x                      sh2a-or-sh4
m68k:548x                      sh2e
m68k:68000                     sh3
m68k:68008                     sh3-dsp
m68k:68010                     sh3-nommu
m68k:68020                     sh3e
m68k:68030                     sh4
m68k:68040                     sh4-nofpu
m68k:68060                     sh4-nommu-nofpu
m68k:cfv4e                     sh4a
m68k:cpu32                     sh4a-nofpu
m68k:fido                      sh4al-dsp
m68k:isa-a                     sh5
m68k:isa-a:emac                simple
m68k:isa-a:mac                 sparc
m68k:isa-a:nodiv               sparc:sparclet
m68k:isa-aplus                 sparc:sparclite
m68k:isa-aplus:emac            sparc:sparclite_le
m68k:isa-aplus:mac             sparc:v8plus
m68k:isa-b                     sparc:v8plusa
m68k:isa-b:emac                sparc:v8plusb
m68k:isa-b:float               sparc:v9
m68k:isa-b:float:emac          sparc:v9a
m68k:isa-b:float:mac           sparc:v9b
m68k:isa-b:mac                 spu:256K
m68k:isa-b:nousp               tomcat
m68k:isa-b:nousp:emac          v850
m68k:isa-b:nousp:mac           v850e
m68k:isa-c                     v850e1
m68k:isa-c:emac                vax
m68k:isa-c:mac                 xscale
m68k:isa-c:nodiv               xstormy16
m68k:isa-c:nodiv:emac          xtensa
set architecture arm:指定arm硬體
  • Toolchain 無法使用 android 的 arm 編譯器目前原因不清楚
https://launchpad.net/linaro-toolchain-binaries/trunk/2013.10/+download/gcc-linaro-arm-linux-gnueabihf-4.8-2013.10_linux.tar.bz2
  • build.env
# -*- shell-script -*-
TOOLCHAIN=gcc-linaro-arm-linux-gnueabihf-4.8-2013.10_linux
DIR=$(pushd $(dirname $BASH_SOURCE) > /dev/null; pwd; popd > /dev/null)
echo $DIR
export PATH=${PATH}:${DIR}/${TOOLCHAIN}/bin
export CC=arm-linux-gnueabihf-gcc

gdbserver , 要注意gdb和gdbserver版本一致

source build.env
./configure --target=arm-linux --host=arm-linux LDFLAGS="-static"
# 這裡的--host指定這個程序的目標平臺。這一步中會檢查系統中是否有交叉編譯器的。
# We must add the above LDFLAGS to let gdb statically linked, otherwise it cannot
run on Android.

time make -j8 2>&1 | tee build.log
file ./gdbserver

./gdbserver: ELF 32-bit LSB  executable, ARM, EABI5 version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 3.1.1, BuildID[sha1]=4eec8a5a6893ed8ce65eea5d0741a55cc621236a, not stripped

cgdb

安裝 cgdb

cgdb 是一個開源的 gdb 前端,可以提供實時的代碼預覽,極大的方便了調試。

獲取源碼

$ git clone git://github.com/cgdb/cgdb.git

依賴

flex( gettext ),autoconf, aclocal, automake, help2man

安裝依賴 (1) flex

$ sudo apt-get install flex

(2) aclocal, automake, autoconf, autoheader 這些 utilities 都在 automake 包中,因此安裝automake 就夠了。

$ sudo apt-get install automake
$ sudo apt-get install autotools-dev

(3) makeinfo,這個 utility 在 texinfo 包中

$ sudo apt-get install texinfo

(3)help2man

$ sudo apt-get install help2man

編譯和安裝

$ cd cgdb
$ ./autogen.sh
$ ./configure --prefix=/usr/local
$ make
$ sudo make install

  • ~/.gdbinit
#    ``                                                                                                    
#   STL GDB evaluators/views/utilities - 1.03
#
#   The new GDB commands:                                                         
#         are entirely non instrumental                                             
#         do not depend on any "inline"(s) - e.g. size(), [], etc
#       are extremely tolerant to debugger settings
#                                                                                 
#   This file should be "included" in .gdbinit as following:
#   source stl-views.gdb or just paste it into your .gdbinit file
#
#   The following STL containers are currently supported:
#
#       std::vector<T> -- via pvector command
#       std::list<T> -- via plist or plist_member command
#       std::map<T,T> -- via pmap or pmap_member command
#       std::multimap<T,T> -- via pmap or pmap_member command
#       std::set<T> -- via pset command
#       std::multiset<T> -- via pset command
#       std::deque<T> -- via pdequeue command
#       std::stack<T> -- via pstack command
#       std::queue<T> -- via pqueue command
#       std::priority_queue<T> -- via ppqueue command
#       std::bitset<n> -- via pbitset command
#       std::string -- via pstring command
#       std::widestring -- via pwstring command
#
#   The end of this file contains (optional) C++ beautifiers
#   Make sure your debugger supports $argc
#
#   Simple GDB Macros writen by Dan Marinescu (H-PhD) - License GPL
#   Inspired by intial work of Tom Malnar, 
#     Tony Novac (PhD) / Cornell / Stanford,
#     Gilad Mishne (PhD) and Many Many Others.
#   Contact: dan_c_marinescu@yahoo.com (Subject: STL)
#
#   Modified to work with g++ 4.3 by Anders Elton
#   Also added _member functions, that instead of printing the entire class in map, prints a member.



#
# std::vector<>
#

define pvector
    if $argc == 0
        help pvector
    else
        set $size = $arg0._M_impl._M_finish - $arg0._M_impl._M_start
        set $capacity = $arg0._M_impl._M_end_of_storage - $arg0._M_impl._M_start
        set $size_max = $size - 1
    end
    if $argc == 1
        set $i = 0
        while $i < $size
            printf "elem[%u]: ", $i
            p *($arg0._M_impl._M_start + $i)
            set $i++
        end
    end
    if $argc == 2
        set $idx = $arg1
        if $idx < 0 || $idx > $size_max
            printf "idx1, idx2 are not in acceptable range: [0..%u].\n", $size_max
        else
            printf "elem[%u]: ", $idx
            p *($arg0._M_impl._M_start + $idx)
        end
    end
    if $argc == 3
      set $start_idx = $arg1
      set $stop_idx = $arg2
      if $start_idx > $stop_idx
        set $tmp_idx = $start_idx
        set $start_idx = $stop_idx
        set $stop_idx = $tmp_idx
      end
      if $start_idx < 0 || $stop_idx < 0 || $start_idx > $size_max || $stop_idx > $size_max
        printf "idx1, idx2 are not in acceptable range: [0..%u].\n", $size_max
      else
        set $i = $start_idx
        while $i <= $stop_idx
            printf "elem[%u]: ", $i
            p *($arg0._M_impl._M_start + $i)
            set $i++
        end
      end
    end
    if $argc > 0
        printf "Vector size = %u\n", $size
        printf "Vector capacity = %u\n", $capacity
        printf "Element "
        whatis $arg0._M_impl._M_start
    end
end

document pvector
    Prints std::vector<T> information.
    Syntax: pvector <vector> <idx1> <idx2>
    Note: idx, idx1 and idx2 must be in acceptable range [0..<vector>.size()-1].
    Examples:
    pvector v - Prints vector content, size, capacity and T typedef
    pvector v 0 - Prints element[idx] from vector
    pvector v 1 2 - Prints elements in range [idx1..idx2] from vector
end 

#
# std::list<>
#

define plist
    if $argc == 0
        help plist
    else
        set $head = &$arg0._M_impl._M_node
        set $current = $arg0._M_impl._M_node._M_next
        set $size = 0
        while $current != $head
            if $argc == 2
                printf "elem[%u]: ", $size
                p *($arg1*)($current + 1)
            end
            if $argc == 3
                if $size == $arg2
                    printf "elem[%u]: ", $size
                    p *($arg1*)($current + 1)
                end
            end
            set $current = $current._M_next
            set $size++
        end
        printf "List size = %u \n", $size
        if $argc == 1
            printf "List "
            whatis $arg0
            printf "Use plist <variable_name> <element_type> to see the elements in the list.\n"
        end
    end
end

document plist
    Prints std::list<T> information.
    Syntax: plist <list> <T> <idx>: Prints list size, if T defined all elements or just element at idx
    Examples:
    plist l - prints list size and definition
    plist l int - prints all elements and list size
    plist l int 2 - prints the third element in the list (if exists) and list size
end

define plist_member
    if $argc == 0
        help plist_member
    else
        set $head = &$arg0._M_impl._M_node
        set $current = $arg0._M_impl._M_node._M_next
        set $size = 0
        while $current != $head
            if $argc == 3
                printf "elem[%u]: ", $size
                p (*($arg1*)($current + 1)).$arg2
            end
            if $argc == 4
                if $size == $arg3
                    printf "elem[%u]: ", $size
                    p (*($arg1*)($current + 1)).$arg2
                end
            end
            set $current = $current._M_next
            set $size++
        end
        printf "List size = %u \n", $size
        if $argc == 1
            printf "List "
            whatis $arg0
            printf "Use plist_member <variable_name> <element_type> <member> to see the elements in the list.\n"
        end
    end
end

document plist_member
    Prints std::list<T> information.
    Syntax: plist <list> <T> <idx>: Prints list size, if T defined all elements or just element at idx
    Examples:
    plist_member l int member - prints all elements and list size
    plist_member l int member 2 - prints the third element in the list (if exists) and list size
end


#
# std::map and std::multimap
#

define pmap
    if $argc == 0
        help pmap
    else
        set $tree = $arg0
        set $i = 0
        set $node = $tree._M_t._M_impl._M_header._M_left
        set $end = $tree._M_t._M_impl._M_header
        set $tree_size = $tree._M_t._M_impl._M_node_count
        if $argc == 1
            printf "Map "
            whatis $tree
            printf "Use pmap <variable_name> <left_element_type> <right_element_type> to see the elements in the map.\n"
        end
        if $argc == 3
            while $i < $tree_size
                set $value = (void *)($node + 1)
                printf "elem[%u].left: ", $i
                p *($arg1*)$value
                set $value = $value + sizeof($arg1)
                printf "elem[%u].right: ", $i
                p *($arg2*)$value
                if $node._M_right != 0
                    set $node = $node._M_right
                    while $node._M_left != 0
                        set $node = $node._M_left
                    end
                else
                    set $tmp_node = $node._M_parent
                    while $node == $tmp_node._M_right
                        set $node = $tmp_node
                        set $tmp_node = $tmp_node._M_parent
                    end
                    if $node._M_right != $tmp_node
                        set $node = $tmp_node
                    end
                end
                set $i++
            end
        end
        if $argc == 4
            set $idx = $arg3
            set $ElementsFound = 0
            while $i < $tree_size
                set $value = (void *)($node + 1)
                if *($arg1*)$value == $idx
                    printf "elem[%u].left: ", $i
                    p *($arg1*)$value
                    set $value = $value + sizeof($arg1)
                    printf "elem[%u].right: ", $i
                    p *($arg2*)$value
                    set $ElementsFound++
                end
                if $node._M_right != 0
                    set $node = $node._M_right
                    while $node._M_left != 0
                        set $node = $node._M_left
                    end
                else
                    set $tmp_node = $node._M_parent
                    while $node == $tmp_node._M_right
                        set $node = $tmp_node
                        set $tmp_node = $tmp_node._M_parent
                    end
                    if $node._M_right != $tmp_node
                        set $node = $tmp_node
                    end
                end
                set $i++
            end
            printf "Number of elements found = %u\n", $ElementsFound
        end
        if $argc == 5
            set $idx1 = $arg3
            set $idx2 = $arg4
            set $ElementsFound = 0
            while $i < $tree_size
                set $value = (void *)($node + 1)
                set $valueLeft = *($arg1*)$value
                set $valueRight = *($arg2*)($value + sizeof($arg1))
                if $valueLeft == $idx1 && $valueRight == $idx2
                    printf "elem[%u].left: ", $i
                    p $valueLeft
                    printf "elem[%u].right: ", $i
                    p $valueRight
                    set $ElementsFound++
                end
                if $node._M_right != 0
                    set $node = $node._M_right
                    while $node._M_left != 0
                        set $node = $node._M_left
                    end
                else
                    set $tmp_node = $node._M_parent
                    while $node == $tmp_node._M_right
                        set $node = $tmp_node
                        set $tmp_node = $tmp_node._M_parent
                    end
                    if $node._M_right != $tmp_node
                        set $node = $tmp_node
                    end
                end
                set $i++
            end
            printf "Number of elements found = %u\n", $ElementsFound
        end
        printf "Map size = %u\n", $tree_size
    end
end

document pmap
    Prints std::map<TLeft and TRight> or std::multimap<TLeft and TRight> information. Works for std::multimap as well.
    Syntax: pmap <map> <TtypeLeft> <TypeRight> <valLeft> <valRight>: Prints map size, if T defined all elements or just element(s) with val(s)
    Examples:
    pmap m - prints map size and definition
    pmap m int int - prints all elements and map size
    pmap m int int 20 - prints the element(s) with left-value = 20 (if any) and map size
    pmap m int int 20 200 - prints the element(s) with left-value = 20 and right-value = 200 (if any) and map size
end


define pmap_member
    if $argc == 0
        help pmap_member
    else
        set $tree = $arg0
        set $i = 0
        set $node = $tree._M_t._M_impl._M_header._M_left
        set $end = $tree._M_t._M_impl._M_header
        set $tree_size = $tree._M_t._M_impl._M_node_count
        if $argc == 1
            printf "Map "
            whatis $tree
            printf "Use pmap <variable_name> <left_element_type> <right_element_type> to see the elements in the map.\n"
        end
        if $argc == 5
            while $i < $tree_size
                set $value = (void *)($node + 1)
                printf "elem[%u].left: ", $i
                p (*($arg1*)$value).$arg2
                set $value = $value + sizeof($arg1)
                printf "elem[%u].right: ", $i
                p (*($arg3*)$value).$arg4
                if $node._M_right != 0
                    set $node = $node._M_right
                    while $node._M_left != 0
                        set $node = $node._M_left
                    end
                else
                    set $tmp_node = $node._M_parent
                    while $node == $tmp_node._M_right
                        set $node = $tmp_node
                        set $tmp_node = $tmp_node._M_parent
                    end
                    if $node._M_right != $tmp_node
                        set $node = $tmp_node
                    end
                end
                set $i++
            end
        end
        if $argc == 6
            set $idx = $arg5
            set $ElementsFound = 0
            while $i < $tree_size
                set $value = (void *)($node + 1)
                if *($arg1*)$value == $idx
                    printf "elem[%u].left: ", $i
                    p (*($arg1*)$value).$arg2
                    set $value = $value + sizeof($arg1)
                    printf "elem[%u].right: ", $i
                    p (*($arg3*)$value).$arg4
                    set $ElementsFound++
                end
                if $node._M_right != 0
                    set $node = $node._M_right
                    while $node._M_left != 0
                        set $node = $node._M_left
                    end
                else
                    set $tmp_node = $node._M_parent
                    while $node == $tmp_node._M_right
                        set $node = $tmp_node
                        set $tmp_node = $tmp_node._M_parent
                    end
                    if $node._M_right != $tmp_node
                        set $node = $tmp_node
                    end
                end
                set $i++
            end
            printf "Number of elements found = %u\n", $ElementsFound
        end
        printf "Map size = %u\n", $tree_size
    end
end

document pmap_member
    Prints std::map<TLeft and TRight> or std::multimap<TLeft and TRight> information. Works for std::multimap as well.
    Syntax: pmap <map> <TtypeLeft> <TypeRight> <valLeft> <valRight>: Prints map size, if T defined all elements or just element(s) with val(s)
    Examples:
    pmap_member m class1 member1 class2 member2 - prints class1.member1 : class2.member2
    pmap_member m class1 member1 class2 member2 lvalue - prints class1.member1 : class2.member2 where class1 == lvalue
end


#
# std::set and std::multiset
#

define pset
    if $argc == 0
        help pset
    else
        set $tree = $arg0
        set $i = 0
        set $node = $tree._M_t._M_impl._M_header._M_left
        set $end = $tree._M_t._M_impl._M_header
        set $tree_size = $tree._M_t._M_impl._M_node_count
        if $argc == 1
            printf "Set "
            whatis $tree
            printf "Use pset <variable_name> <element_type> to see the elements in the set.\n"
        end
        if $argc == 2
            while $i < $tree_size
                set $value = (void *)($node + 1)
                printf "elem[%u]: ", $i
                p *($arg1*)$value
                if $node._M_right != 0
                    set $node = $node._M_right
                    while $node._M_left != 0
                        set $node = $node._M_left
                    end
                else
                    set $tmp_node = $node._M_parent
                    while $node == $tmp_node._M_right
                        set $node = $tmp_node
                        set $tmp_node = $tmp_node._M_parent
                    end
                    if $node._M_right != $tmp_node
                        set $node = $tmp_node
                    end
                end
                set $i++
            end
        end
        if $argc == 3
            set $idx = $arg2
            set $ElementsFound = 0
            while $i < $tree_size
                set $value = (void *)($node + 1)
                if *($arg1*)$value == $idx
                    printf "elem[%u]: ", $i
                    p *($arg1*)$value
                    set $ElementsFound++
                end
                if $node._M_right != 0
                    set $node = $node._M_right
                    while $node._M_left != 0
                        set $node = $node._M_left
                    end
                else
                    set $tmp_node = $node._M_parent
                    while $node == $tmp_node._M_right
                        set $node = $tmp_node
                        set $tmp_node = $tmp_node._M_parent
                    end
                    if $node._M_right != $tmp_node
                        set $node = $tmp_node
                    end
                end
                set $i++
            end
            printf "Number of elements found = %u\n", $ElementsFound
        end
        printf "Set size = %u\n", $tree_size
    end
end

document pset
    Prints std::set<T> or std::multiset<T> information. Works for std::multiset as well.
    Syntax: pset <set> <T> <val>: Prints set size, if T defined all elements or just element(s) having val
    Examples:
    pset s - prints set size and definition
    pset s int - prints all elements and the size of s
    pset s int 20 - prints the element(s) with value = 20 (if any) and the size of s
end



#
# std::dequeue
#

define pdequeue
    if $argc == 0
        help pdequeue
    else
        set $size = 0
        set $start_cur = $arg0._M_impl._M_start._M_cur
        set $start_last = $arg0._M_impl._M_start._M_last
        set $start_stop = $start_last
        while $start_cur != $start_stop
            p *$start_cur
            set $start_cur++
            set $size++
        end
        set $finish_first = $arg0._M_impl._M_finish._M_first
        set $finish_cur = $arg0._M_impl._M_finish._M_cur
        set $finish_last = $arg0._M_impl._M_finish._M_last
        if $finish_cur < $finish_last
            set $finish_stop = $finish_cur
        else
            set $finish_stop = $finish_last
        end
        while $finish_first != $finish_stop
            p *$finish_first
            set $finish_first++
            set $size++
        end
        printf "Dequeue size = %u\n", $size
    end
end

document pdequeue
    Prints std::dequeue<T> information.
    Syntax: pdequeue <dequeue>: Prints dequeue size, if T defined all elements
    Deque elements are listed "left to right" (left-most stands for front and right-most stands for back)
    Example:
    pdequeue d - prints all elements and size of d
end



#
# std::stack
#

define pstack
    if $argc == 0
        help pstack
    else
        set $start_cur = $arg0.c._M_impl._M_start._M_cur
        set $finish_cur = $arg0.c._M_impl._M_finish._M_cur
        set $size = $finish_cur - $start_cur
        set $i = $size - 1
        while $i >= 0
            p *($start_cur + $i)
            set $i--
        end
        printf "Stack size = %u\n", $size
    end
end

document pstack
    Prints std::stack<T> information.
    Syntax: pstack <stack>: Prints all elements and size of the stack
    Stack elements are listed "top to buttom" (top-most element is the first to come on pop)
    Example:
    pstack s - prints all elements and the size of s
end



#
# std::queue
#

define pqueue
    if $argc == 0
        help pqueue
    else
        set $start_cur = $arg0.c._M_impl._M_start._M_cur
        set $finish_cur = $arg0.c._M_impl._M_finish._M_cur
        set $size = $finish_cur - $start_cur
        set $i = 0
        while $i < $size
            p *($start_cur + $i)
            set $i++
        end
        printf "Queue size = %u\n", $size
    end
end

document pqueue
    Prints std::queue<T> information.
    Syntax: pqueue <queue>: Prints all elements and the size of the queue
    Queue elements are listed "top to bottom" (top-most element is the first to come on pop)
    Example:
    pqueue q - prints all elements and the size of q
end



#
# std::priority_queue
#

define ppqueue
    if $argc == 0
        help ppqueue
    else
        set $size = $arg0.c._M_impl._M_finish - $arg0.c._M_impl._M_start
        set $capacity = $arg0.c._M_impl._M_end_of_storage - $arg0.c._M_impl._M_start
        set $i = $size - 1
        while $i >= 0
            p *($arg0.c._M_impl._M_start + $i)
            set $i--
        end
        printf "Priority queue size = %u\n", $size
        printf "Priority queue capacity = %u\n", $capacity
    end
end

document ppqueue
    Prints std::priority_queue<T> information.
    Syntax: ppqueue <priority_queue>: Prints all elements, size and capacity of the priority_queue
    Priority_queue elements are listed "top to buttom" (top-most element is the first to come on pop)
    Example:
    ppqueue pq - prints all elements, size and capacity of pq
end



#
# std::bitset
#

define pbitset
    if $argc == 0
        help pbitset
    else
        p /t $arg0._M_w
    end
end

document pbitset
    Prints std::bitset<n> information.
    Syntax: pbitset <bitset>: Prints all bits in bitset
    Example:
    pbitset b - prints all bits in b
end



#
# std::string
#

define pstring
    if $argc == 0
        help pstring
    else
        printf "String \t\t\t= \"%s\"\n", $arg0._M_data()
        printf "String size/length \t= %u\n", $arg0._M_rep()._M_length
        printf "String capacity \t= %u\n", $arg0._M_rep()._M_capacity
        printf "String ref-count \t= %d\n", $arg0._M_rep()._M_refcount
    end
end

document pstring
    Prints std::string information.
    Syntax: pstring <string>
    Example:
    pstring s - Prints content, size/length, capacity and ref-count of string s
end 

#
# std::wstring
#

define pwstring
    if $argc == 0
        help pwstring
    else
        call printf("WString \t\t= \"%ls\"\n", $arg0._M_data())
        printf "WString size/length \t= %u\n", $arg0._M_rep()._M_length
        printf "WString capacity \t= %u\n", $arg0._M_rep()._M_capacity
        printf "WString ref-count \t= %d\n", $arg0._M_rep()._M_refcount
    end
end

document pwstring
    Prints std::wstring information.
    Syntax: pwstring <wstring>
    Example:
    pwstring s - Prints content, size/length, capacity and ref-count of wstring s
end 

#
# C++ related beautifiers (optional)
#

set height 0
set history size 10000
set history filename ~/.gdb_history
set history save on

#退出時不顯示提示信息
set confirm off

#按照派生類型打印對象
set print object on

#打印數組的索引下標
set print array-indexes on

#每行打印一個結構體成員
set print pretty on

set print union on

set print address on 

set print static-members on
set print vtbl on
set print demangle on
set demangle-style gnu-v3
set print sevenbit-strings off
set step-mode on

shell rm -f ./gdb.log
set logging off
set logging file ./gdb.log
set logging on

python
import sys
import os
p = os.path.expanduser('/home/shihyu/.mybin/gdb_8.1/python') 
print(p)
if os.path.exists(p):
    sys.path.insert(0, p)
    from libstdcxx.v6.printers import register_libstdcxx_printers
    register_libstdcxx_printers(None)
end

core dump

# 查看當前系統允許的 Core Dump 文件大小限制
# 如果顯示 "0",表示 Core Dump 文件生成被禁用
ulimit -c

# 設置 Core Dump 文件大小無限制
# 這條命令允許程序崩潰時生成 Core Dump 文件,便於調試
ulimit -c unlimited

# 停止 Apport 服務,Apport 是 Ubuntu 的錯誤報告系統
# Ubuntu 默認使用 Apport 來攔截崩潰錯誤,會干擾 Core Dump 文件的生成
# 通過停止 Apport,可以讓系統直接生成標準的 Core Dump 文件
sudo service apport stop

100個gdb小技巧

  • https://github.com/hellogcc/100-gdb-tips

GDB常用指令

  • GDB dashboard
https://github.com/cyrus-and/gdb-dashboard
  • Gdbinit for OS X, iOS and others - x86, x86_64 and ARM
https://github.com/gdbinit/Gdbinit
  • dotgdb:關於底層調試和反向工程的gdb腳本集
https://github.com/dholm/dotgdb

打印記憶體內容

用gdb查看內存 格式: x /nfu 說明

x 是 examine 的縮寫
n表示要顯示的內存單元的個數
f表示顯示方式, 可取如下值
x 按十六進制格式顯示變量。
d 按十進制格式顯示變量。
u 按十進制格式顯示無符號整型。
o 按八進制格式顯示變量。
t 按二進制格式顯示變量。
a 按十六進制格式顯示變量。
i 指令地址格式
c 按字符格式顯示變量。
f 按浮點數格式顯示變量。
u表示一個地址單元的長度
b表示單字節,
h表示雙字節,
w表示四字節,
g表示八字節

Format letters are 
o(octal), 
x(hex), 
d(decimal), 
u(unsigned decimal),
t(binary), 
f(float), 
a(address), 
i(instruction), 
c(char) and 
s(string).
Size letters are 
b(byte), 
h(halfword), 
w(word), 
g(giant, 8 bytes)

舉例

x/3uh buf 
表示從內存地址buf讀取內容,
h表示以雙字節為一個單位,
3表示三個單位,
u表示按十六進制顯示
例子:
n是個局部變量
Breakpoint 1, main (argc=1, argv=0xbffff3a4) at calc.c:7
7        int n = atoi(argv[1]);
(gdb) print &n
$1 = (int *) 0xbffff2ec
(gdb) x 0xbffff2ec
0xbffff2ec:    0x00282ff4
(gdb) print * (int *) 0xbffff2ec
$2 = 2633716
(gdb) x /4xw 0xbffff2ec
0xbffff2ec:    0x00282ff4    0x080484e0    0x00000000    0xbffff378
(gdb) x /4dw 0xbffff2ec
0xbffff2ec:    2633716    134513888    0    -1073745032
(gdb)

跳轉執行

一般來說,被調試程序會按照程序代碼的運行順序依次執行。 GDB提供了亂序執行的功能,也就是說,GDB可以修改程序的執行順序,可以讓程序執行隨意跳躍。 這個功能可以由GDB的jump命令來完:

jump <linespec>

指定下一條語句的運行點。可以是文件的行號,可以是file:line格式,可以是+num這種偏移量格式。 表式著下一條運行語句從哪裡開始。

jump <address>
這裡的 <address> 是代碼行的內存地址。

注意,jump命令不會改變當前的程序棧中的內容,所以,當你從一個函數跳到另一個函數時,當函數運行完返回時進行彈棧操作時必然會發生錯誤,可能結果還是非常奇怪的,甚至於產生程序Core Dump。 所以最好是同一個函數中進行跳轉。

熟悉彙編的人都知道,程序運行時,有一個寄存器用於保存當前。 所以,jump命令也就是改變了這個寄存器中的值。 於是,你可以使用「set $pc」來更改跳轉執行的地址。如:set $pc = 0x485`

  • 以便快速找出所有線程都在做。
thread apply all bt或 thread apply all print $pc
  • 不斷測試,之後會出現segmentation fault(core dump)
while ./bug_program ; do echo OK; done

高級技巧

一些不太廣為人知的技巧...

加載獨立的調試信息

gdb調試的時候可以從單獨的符號文件中加載調試信息。

(gdb) exec-file test
(gdb) symbol-file test.debug

test是移除了調試信息的可執行文件, test.debug是被移除後單獨存儲的調試信息。參考stackoverflow上的一個問題,可以如下分離調試信息:

編譯程序,帶調試信息(-g)

gcc -g -o test main.c
```sh
### 拷貝調試信息到test.debug

objcopy --only-keep-debug test test.debug

### 移除test中的調試信息
```sh
strip --strip-debug --strip-unneeded test

然後啟動gdb

gdb -s test.debug -e test

或這樣啟動gdb

gdb
(gdb) exec-file test
(gdb) symbol-file test.debug

分離出的調試信息test.debug還可以鏈接回可執行文件test中

objcopy --add-gnu-debuglink test.debug test

然後就可以正常用addr2line等需要讀取調試信息的程序了

addr2line -e test 0x401c23

更多內容可閱讀GDB: Debugging Information in Separate Files。

在內存和文件系統之間拷貝數據

  • 將內存數據拷貝到文件裡

    dump binary value file_name variable_name
    dump binary memory file_name begin_addr end_addr
    
  • 改變內存數據

    使用set命令
    

1. 啟動

gdb 應用程序名
gdb 應用程序名 core文件名
gdb 應用程序名 pid
gdb --args 應用程序名 應用程序的運行參數

幫助:

help 顯示幫助
info 顯示程序狀態
set 修改
show 顯示gdb狀態

運行及運行環境設置:

set args # 設置運行參數
show args # 顯示運行參數

set env 變量名 = 值 # 設置環境變量
unset env [變量名] # 取消環境變量
show env [變量名] # 顯示環境變量

path 目錄名 # 把目錄添加到查找路徑中
show paths # 顯示當前查找路徑

cd 目錄 # 切換工作目錄
pwd # 顯示當前工作目錄

tty /dev/pts/1 # 修改程序的輸入輸出到指定的tty
set inferior-tty /dev/pts/1 # 修改程序的輸出到指定的tty
show inferior-tty
show tty

run 參數 # 運行
start 參數 # 開始運行,但是會在main函數停止
attach pid
detach

kill # 退出
Ctrl-C # 中斷(SIGINT)
Ctrl-]

線程操作:

info threads # 查看所有線程信息
thread 線程id # 切換到指定線程
thread apply [threadno | all ] 參數 # 對所有線程都應用某個命令

子進程調試:

set follow-fork-mode child|parent # fork後,需要跟蹤誰
show follow-fork-mode
set detach-on-flow on|off # fork後,需要兩個都跟蹤嗎

info forks # 顯示所有進程信息
fork 進程id # 切換到某個進程
detach-fork 進程id # 不再跟蹤某個進程
delete fork 進程id # kill某個進程並停止對它的跟蹤

檢查點:

checkpoint/restart

查看停止原因:

info program

斷點(breakpoint): 程序運行到某處就會中斷

break(b) 行號|函數名|程序地址 | +/-offset | filenam:func [if 條件] # 在指定位置設置斷點
tbreak ... # 與break相似,只是設置一次斷點
hbreak ... # 與break相似,只是設置硬件斷點,需要硬件支持
thbreak ... # 與break相似,只是設置一次性硬件斷點,需要硬件支持
rbreak 正則表達式 # 給一批滿足條件的函數打上斷點
info break [斷點號] # 查看某個斷點或所有斷點信息

set breadpoint pending auto|on|off # 查看如果斷點位置沒有找到時行為
show breakpoint pending

觀察點(watchpoint): 表達式的值修改時會被中斷

watch 表達式 # 當表達式被寫入,並且值被改變時中斷
rwatch 表達式 # 當表達式被讀時中斷
awatch 表達式 # 當表達式被讀或寫時中斷
info watchpoints

set can-use-hw-watchpoints 值 # 設置使用的硬件觀察點的數
show can-use-hw-watchpoints

rwatch與awatch需要有硬件支持,另外如果是對局部變量使用watchpoint,那退出作用域時觀察點會自動被刪除
另外在多線程情況下,gdb的watchpoint只對一個線程有效

捕獲點(catchpoint): 程序發生某個事件時停止,如產生異常時

catch 事件名
事件包括:
throw # 產生c++異常
catch # 捕獲到c++異常
exec/fork/vfork # 一個exec/fork/vfork函數調用,只對HP-UX
load/unload [庫名] # 加載/卸載共享庫事件,對只HP-UX
tcatch 事件名 # 一次性catch
info break

斷點操作:

clear [函數名|行號] # 刪除斷點,無參數表示刪衛當前位置
delete [斷點號] # 刪除斷點,無參數表示刪所有斷點
disable [斷點號]
enable [斷點號]


condition 斷點號 條件 # 增加斷點條件
condition 斷點號 # 刪除斷點條件

ignore 斷點號 數目 # 忽略斷點n次

commands 斷點號 # 當某個斷點中斷時打印條件
條件
end
  • 下面是一個例子,可以一直打印當前的X值:
commands 3
printf "X:%d\n",x
cont
end

斷點後操作:

continue(c) [忽略次數] # 繼續執行,[忽略前面n次中斷]
fg [忽略次數] # 繼續執行,[忽略前面n次中斷]

step(s) [n步] # 步進,重複n次
next(n) [n步] # 前進,重複n次

finish # 完成當前函數調用,一直執行到返回處,並打印返回值

until(u) [位置] # 一直執行到當前行或指定位置,或是當前函數返回
advance 位置 # 前面到指定位置,如果當前函數返回則停止,與until類似

stepi(si) [n步] # 精確的只執行一個彙編指令,重複n次
nexti(ni) [n步] # 精確的只執行一個彙編指令,碰到函數跳過,重複n次

set step-mode on|off # on時,如果函數沒有調試信息也跟進
show step-mode

信號:

info signals # 列出所有信號的處理方式
info handle # 同上

handle 信號 方式 # 改變當前處理某個信號的方式
方式包括:
nostop # 當信號發生時不停止,只打印信號曾經發生過
stop # 停止並打印信號
print # 信號發生時打印
noprint # 信號發生時不打印
pass/noignore # gdb充許應用程序看到這個信號
nopass/ignore # gdb不充許應用程序看到這個信號

線程斷點:

break 行號信息 thread 線程號 [if 條件] # 只在某個線程內加斷點

線程調度鎖:

set scheduler-locking on|off # off時所有線程都可以得到調度,on時只有當前
show scheduler-locking

幀:

frame(f) [幀號] # 不帶參數時顯示所有幀信息,帶參數時切換到指定幀
frame 地址 # 切換到指定地址的幀
up [n] # 向上n幀
down [n] # 向下n幀

select-frame 幀號 # 切換到指定幀並且不打印被轉換到的幀的信息
up-silently [n] # 向上n幀,不顯示幀信息
down-silently [n] # 向下n幀,不顯示幀信息

調用鏈:

backtrace(bt) [n|-n|full] # 顯示當前調用鏈,n限制顯示的數目,-n表示顯示後n個,n表示顯示前n個,full的話還會顯示變量信息
使用 thread apply all bt 就可以顯示所有線程的調用信息

set backtrace past-main on|off
show backtrace past-main

set backtrace past-entry on|off
show backtrace past-entry

set backtrace limit n # 限制調用信息的顯示層數
show backtrace limit

顯示幀信息:

info frame # 顯示當前幀信息
info frame addr # 顯示指定地址的幀信息
info args # 顯示幀的參數
info locals # 顯示局部變量信息
info catch # 顯示本幀異常信息

顯示行號:

list(l) [行號|函數|文件:行號] # 顯示指定位置的信息,無參數為當前位置
list - # 顯示當前行之前的信息

list first,last # 從frist顯示到last行
list ,last # 從當前行顯示到last行
list frist, # 從指定行顯示
list + # 顯示上次list後顯示的內容
list - # 顯示上次list前面的內容

在上面,first和last可以是下面類型:
行號
+偏移
-偏移
文件名:行號
函數名
函數名:行號

set listsize n # 修改每次顯示的行數
show listsize

編輯:

edit [行號|函數|函數名:行號|文件名:函數名] # 編輯指定位置

查找:

search 表示式 # 向前查找表達式
reverse-search 表示式 # 向後查找表達式

指定源碼目錄:

directory(dir) [目錄名] # 指定源文件查找目錄
show directories

源碼與機器碼:

info line [函數名|行號] # 顯示指定位置對應的機器碼地址範圍
disassemble [函數名 | 起始地址 結束地址] # 對指定範圍進行反彙編
set disassembly-flavor att|intel # 指定彙編代碼形式
show disassembly-flavor

查看數據:

ptype 表達式 # 查看某個表達式的類型

print [/f] [表達式] # 按格式查看錶達式內容,/f是格式化

set print address on|off # 打印時是不是顯示地址信息
show print address

set print symbol-filename on|off # 是不是顯示符號所在文件等信息
show print symbol-filename

set print array on | off # 是不是打印數組
show print array

set print array index on | off # 是不是打印下標
show print array index

...

表達式可以用下面的修飾符:
var@n # 表示把var當成長度為n的數組
filename::var # 表示打印某個函數內的變量,filename可以換成其它範圍符如文件名
{type} var # 表示把var當成type類型

輸出格式:

x # 16進制
d # 10進制
u # 無符號
o # 8進制
t # 2進制
a # 地址
c # 字符
f # 浮點

查看內存:

x /nfu 地址 # 查看內存
n 重複n次
f 顯示格式,為print使用的格式
u 每個單元的大小,為
b byte
h 2 byte
w 4 byte
g 8 byte

自動顯示:

display [/fmt] 表達式 # 每次停止時都會顯示錶達式,fmt與print的格式一樣,如果是內存地址,那fmt可像 x的參數一樣
undisplay 顯示編號
delete display 顯示編號 # 這兩個都是刪附某個顯示

disable display 顯示編號 # 禁止某個顯示
enable display 顯示編號 # 重顯示
display # 顯示當前顯示內容
info display # 查看所有display項

查看變量歷史:

show values 變量名 [n] # 顯示變量的上次顯示歷史,顯示n條
show values 變量名 + # 繼續上次顯示內容

便利變量: (聲明變量的別名以方便使用)

set $foo = *object_ptr # 聲明foo為object_ptr的便利變量
init-if-undefined $var = expression # 如果var還未定義則賦值
show convenience

內部便利變量:

$_ 上次x查看的地址
$__
$_exitcode 程序垢退出碼

寄存器:

into registers # 除了浮點寄存器外所有寄存器
info all-registers # 所有寄存器
into registers 寄存器名 # 指定寄存器內容
info float # 查看浮點寄存器狀態
info vector # 查看向量寄存器狀態

gdb為一些內部寄存器定義了名字,如$pc(指令),$sp(棧指針),$fp(棧幀),$ps(程序狀態)
p /x $pc # 查看pc寄存器當前值
x /i $pc # 查看要執行的下一條指令
set $sp += 4 # 移動棧指針

內核中信息:

info udot # 查看內核中user struct信息
info auxv # 顯示auxv內容(auxv是協助程序啟動的環境變量的)

內存區域限制:

mem 起始地址 結構地址 屬性 # 對[地始地址,結構地址)區域內存進行保護,如果結構地址為0表示地址最大值0xffffffff
delete mem 編號 # 刪除一個內存保護
disable mem 編號 # 禁止一個內存保護
enable mem 編號 # 打開一個內存保護
info mem # 顯示所有內存保護信息

保護的屬性包括:

1. 內存訪問模式: ro | wo |rw
2. 內存訪問大小: 8 | 16 | 32 | 64 如果不限制,表示可按任意大小訪問
3. 數據緩存: cache | nocache cache表示充許gdb緩存目標內存

內存複製到/從文件:

dump [格式] memory 文件名 起始地址 結構地址 # 把指定內存段寫到文件
dump [格式] value 文件名 表達式 # 把指定值寫到文件
格式包括:
binary 原始二進制格式
ihex intel 16進制格式
srec S-recored格式
tekhex tektronix 16進制格式

append [binary] memory 文件名 起始地址 結構地址 # 按2進制追加到文件
append [binary] value 文件名 表達式 # 按2進制追加到文件

restore 文件名 [binary] bias 起始地址 結構地址 # 恢復文件中內容到內存.如果文件內容是原始二進制,需要指定binary參數,不然會gdb自動識別文件格式

產生core dump文件

gcore [文件名] # 產生core dump文件

字符集:

set target-charset 字符集 # 聲明目標機器的locale,如gdbserver所在機器
set host-charset 字符集 # 聲明本機的locale
set charset 字符集 # 聲明目標機和本機的locale
show charset
show host-charset
show target-charset

緩存遠程目標的數據:為提高性能可以使用數據緩存,不過gdb不知道volatile變量,緩存可能會顯示不正確的結構

set remotecache on | off
show remotecache
info dcache # 顯示數據緩存的性能

C預處理宏:

macro expand(exp) 表達式 # 顯示宏展開結果
macro expand-once(expl) 表達式 # 顯示宏一次展開結果
macro info 宏名 # 查看宏定義

追蹤(tracepoint): 就是在某個點設置採樣信息,每次經過這個點時只執行已經定義的採樣動作但並不停止,最後再根據採樣結果進行分析。

採樣點定義:

trace 位置 # 定義採樣點
info tracepoints # 查看採樣點列表
delete trace 採樣點編號 # 刪除採傑點
disable trace 採樣點編號 # 禁止採傑點
enable trace 採樣點編號 # 使用採傑點
passcount 採樣點編號 [n] # 當通過採樣點 n次後停止,不指定n則在下一個斷點停止

預定義動作:預定義動作以actions開始,後面是一系列的動作

actions [num] # 對採樣點num定義動作
行為:
collect 表達式 # 採樣表達式信息
一些表達式有特殊意義,如$regs(所有寄存器),$args(所有函數參數),$locals(所有局部變量)
while-steping n # 當執行第n次時的動作,下面跟自己的collect操作

採樣控制:

tstart # 開始採樣
tstop # 停止採樣
tstatus # 顯示當前採樣的數據

使用收集到的數據:

tfind start # 查找第一個記錄
tfind end | none # 停止查找
tfind # 查找下一個記錄
tfind - # 查找上一個記錄
tfind tracepoint N # 查找 追蹤編號為N 的下一個記錄
tfind pc 地址 # 查找代碼在指定地址的下一個記錄
tfind range 起始,結束
tfind outside 起始,結構
tfind line [文件名:]行號

tdump # 顯示當前記錄中追蹤信息
save-tracepoints 文件名 # 保存追蹤信息到指定文件,後面使用source命令讀

追蹤中的便利變量:

$trace_frame # 當前幀編號, -1表示沒有, INT
$tracepoint # 當前追蹤,INT
$trace_line # 位置 INT
$trace_file # 追蹤文件 string, 需要使用output輸出,不應用printf
$trace_func # 函數名 string

覆蓋技術(overray): 用於調試過大的文件

gdb文件:

file 文件名 # 加載文件,此文件為可執行文件,並且從這裡讀取符號
core 文件名 # 加載core dump文件
exec-file 文件名 # 加載可執行文件
symbol-file 文件名 # 加載符號文件
add-symbol-file 文件名 地址 # 加載其它符號文件到指定地址
add-symbol-file-from-memory 地址 # 從指定地址中加載符號
add-share-symbol-files 庫文件 # 只適用於cygwin
session 段 地址 # 修改段信息
info files | target # 打開當前目標信息
maint info sections # 查看程序段信息
set truct-readonly-sections on | off # 加快速度
show truct-readonly-sections

set auto-solib-add on | off # 修改自動加載動態庫的模式
show auto-solib-add

info share # 打印當前加載的共享庫的地址信息
share [正則表達式] # 從符合的文件中加載共享庫的正則表達式

set stop-on-solib-events on | off # 設置當加載共享庫時是不是要停止
show stop-on-solib-events

set solib-absolute-prefix 路徑 # 設置共享庫的絕對路矩,當加載共享庫時會以此路徑下查找(類似chroot)
show solib-absolute-prefix

set solib-search-path 路徑 # 如果solib-absolute-prefix查找失敗,那將使用這個目錄查找共享庫
show solib-search-path

jemalloc源碼分析之分析工具

簡介

jemalloc同malloc一樣, 是一種內存管理的實現.

如果使用gcc編譯軟件, 默認使用的是glic實現的ptmalloc算法. 而同樣的算法有google的C++實現tcmalloc算法, 而今天我們分析的是facebook使用C語言實現的jemalloc算法.

tcmalloc同jemalloc一樣都是對多線程多核友好的分配算法, 被各種語言借鑑來實現自身的內存管理.

實現原理

如果使用C語言進行內存分配, 我們會調用malloc函數, 而jemalloc就是通過malloc的hook機制實現的.

如何實現自定義的malloc函數 這篇文章有介紹如何覆蓋或重寫默認的malloc函數.

GNU基於hook機制實現自定義的的malloc函數, 具體就是通過覆蓋__malloc_hook 函數指標來實現的.

在jemalloc中我們能找到類似的代碼:

jemalloc.c:1830

/*
 * Begin non-standard override functions.
 */

#ifdef JEMALLOC_OVERRIDE_MEMALIGN
JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN
void JEMALLOC_NOTHROW*
JEMALLOC_ATTR(malloc)
je_memalign(size_t alignment, size_t size)
{
    void* ret JEMALLOC_CC_SILENCE_INIT(NULL);

    if (unlikely(imemalign(&ret, alignment, size, 1) != 0)) {
        ret = NULL;
    }

    return (ret);
}
#endif

#ifdef JEMALLOC_OVERRIDE_VALLOC
JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN
void JEMALLOC_NOTHROW*
JEMALLOC_ATTR(malloc)
je_valloc(size_t size)
{
    void* ret JEMALLOC_CC_SILENCE_INIT(NULL);

    if (unlikely(imemalign(&ret, PAGE, size, 1) != 0)) {
        ret = NULL;
    }

    return (ret);
}
#endif

/*
 * is_malloc(je_malloc) is some macro magic to detect if jemalloc_defs.h has
 * #define je_malloc malloc
 */
#define malloc_is_malloc 1
#define is_malloc_(a) malloc_is_ ## a
#define is_malloc(a) is_malloc_(a)

#if ((is_malloc(je_malloc) == 1) && defined(JEMALLOC_GLIBC_MALLOC_HOOK))
/*
 * glibc provides the RTLD_DEEPBIND flag for dlopen which can make it possible
 * to inconsistently reference libc's malloc(3)-compatible functions
 * (https://bugzilla.mozilla.org/show_bug.cgi?id=493541).
 *
 * These definitions interpose hooks in glibc.  The functions are actually
 * passed an extra argument for the caller return address, which will be
 * ignored.
 */
JEMALLOC_EXPORT void (*__free_hook)(void* ptr) = je_free;
JEMALLOC_EXPORT void* (*__malloc_hook)(size_t size) = je_malloc;
JEMALLOC_EXPORT void* (*__realloc_hook)(void* ptr, size_t size) = je_realloc;
# ifdef JEMALLOC_GLIBC_MEMALIGN_HOOK
JEMALLOC_EXPORT void* (*__memalign_hook)(size_t alignment, size_t size) =
    je_memalign;
# endif
#endif

/*
 * End non-standard override functions.
 */

如果我們在自己的函數調用malloc就會被je_malloc攔截. 例如下面的例子:

int main()
{
    void* ptr = malloc(10);
    free(ptr);
    return 0;
}

整個過程是

-> main
-> malloc -> je_malloc(mmap等系統調用分配內存) -> malloc結束
-> free -> jeje_free(munmap等系統調用釋放內存) -> free結束
-> main結束

上面是當我們程序調用malloc函數時執行的過程, 實際上在jemalloc載入的時候, 就已經進行了一些初始化操作.

具體是在jemalloc_constructor函數.

jemalloc.c:2576

#ifndef JEMALLOC_JET
JEMALLOC_ATTR(constructor)
static void
jemalloc_constructor(void)
{

    malloc_init();
}
#endif

jemalloc_macros.h.in:67
#  define JEMALLOC_ATTR(s) __attribute__((s))

通過這篇文章 如何在共享庫載入時進行初始化操作 知道這是gcc的一個特性.

後面我們將結合"call graph"調用圖分別分析這兩個過程.

開始調試

這節主要介紹下載編譯jemalloc, 編寫測試代碼, 使用callgrind生成調用圖, 使用gdb調試jemalloc.

jemalloc當前託管在github上

git clone git@github.com:jemalloc/jemalloc.git
./autogen.sh
./configure --enable-debug
make dist
make
make install

然後使用ide添加jemalloc項目, 主要作用是方便查看源代碼, 在gdb中查看源代碼實在不太方便, 而且gdb-tui雖然提供了可視化界面, 但是偶爾會出現花屏的情況.

這中間可能因為doc文檔找不到的原因安裝失敗, 根據issue231, 將最後兩步換成

make && make install_bin install_include install_lib

即可.

然後編寫我們的調試代碼:

a.c文件:

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>

int func_long_name_a();
int func_long_name_b();
int func_long_name_c();

int func_long_name_a()
{
    printf("func_long_name_a called\n");
    func_long_name_b();
    return 0;
}

int func_long_name_b()
{
    printf("func_long_name_b called\n");
    func_long_name_c();
    return 0;
}

int func_long_name_c()
{
    printf("func_long_name_c called\n");
    int sizeArr[] = {1, 4095, 4096, 8192, 8193, 4 * 1024 * 1024, 10 * 1024 * 1024};
    int i;

    for (i = 0; i < 7; ++i) {
        void* p = malloc(sizeArr[i]);
        free(p);
    }

    return 0;
}

int main(int argc, char** argv)
{
    printf("main called\n");
    func_long_name_a();
    func_long_name_c();
    printf("main exit\n");
    return 0;
}

然後編寫一個腳本來實現編譯及調用圖的生成

gen.sh文件

#!/bin/bash

JEMALLOC_PATH=/home/shihyu/github/jemalloc
gcc -g -ljemalloc -o a -I${JEMALLOC_PATH}/include -L${JEMALLOC_PATH}/lib a.c
valgrind --tool=callgrind ./a
gprof2dot -f callgrind -n 0 callgrind.out.* | dot -Tsvg -o a.svg
date=`date '+%Y%m%d%H%i%s'`
mv a.svg "$(date '+%Y-%m-%d_%H:%M:%S').svg"
#rm -f callgrind.out.* .DS_Store a a.out
rm -f callgrind.out.* .DS_Store a.out
echo
ls -al .

中間需要安裝一些特別的軟件, 比如valgrind, gprof2dot, dot等, 這些都可以在網上找到相應的安裝方法.

最後生成我們的jemalloc_call_graph.svg調用圖文件.

在gen.sh中我們並沒有刪除可執行文件"a", 下面我就使用gdb來調試該文件.

# gdb a
# b jemalloc_constructor
# b src/jemalloc.c:1443
# b src/jemalloc.c:1811
# r

其中jemalloc_constructor是jemalloc共享庫載入時的入口.

src/jemalloc.c:1443是je_malloc函數實現的地方.

src/jemalloc.c:1811是je_free函數實現的地方.

可根據自己的jemalloc版本找到兩個函數的行數做出調整.

執行r後, gdb就停在了jemalloc_constructor函數處.

關於gdb的使用, 也很多, 這裡也有關於gdb可視化界面gdb-tui的使用.

其中在tui模式和傳統模式切換的快捷鍵是ctrl+x接ctrl+a.

總結

這篇文章主要介紹瞭如何調試jemalloc, 是分析jemalloc的準備工作, 也是分析其他開源c程序的普遍方法.

首先使用valgrind+dot打印函數調用圖, 找到函數執行的流程. 然後分析基礎的數據結構與其附屬的操作, 快速明白各種變量會有怎樣的轉換. 最後順著調用圖, 分析各個函數的實現, 以及各種結構體之間的關系. 至此, 所有的源代碼幾乎查看完畢, 一個軟件也分析完畢.


使用jemalloc來對c,c++程序進行內存管理

git clone https://github.com/jemalloc/jemalloc

cd jemalloc

注意:這一步確定要把jemalloc的函數編譯成哪種形式,比如下面的配置就會把分配內存的函數編譯成je_malloc的形式,把calloc編譯成je_calloc等等。這樣就不會和系統的libc的分配函數malloc沖突,因為若不指定該選項默認編譯的分配函數是malloc。

 ./configure --enable-debug --with-jemalloc-prefix=je_

make -j8

使用jemalloc

mkdir jem_test
#include <stdio.h>
#include <jemalloc/jemalloc.h>
//define to jemalloc
#define malloc(size) je_malloc(size)
#define calloc(count,size) je_calloc(count,size)
#define realloc(ptr,size) je_realloc(ptr,size)
#define free(ptr) je_free(ptr)

int main(void)
{
    char* pcon;

    pcon = malloc(10 * sizeof(char));

    if (!pcon) {
        fprintf(stderr, "malloc failed!\n");
    }

    if (pcon != NULL) {
        free(pcon);
        pcon = NULL;
    }

    fprintf(stderr, "main end!\n");
    return 0;
}
CC=gcc
CFLAGS=-Wall -g
INCLUDES=-I /home/shihyu/github/jemalloc/include
ALLOC_DEP=/home/shihyu/github/jemalloc/lib/libjemalloc.a
ALLOC_LINK=$(ALLOC_DEP) -lpthread -ldl

dtest: dtest.o
	$(CC) $(INCLUDES) $(CFLAGS) -o dtest dtest.o $(ALLOC_LINK)

dtest.o: dtest.c $(ALLOC_DEP)
	$(CC) -c $(INCLUDES) $(CFLAGS) dtest.c

clean:
	rm -f dtest dtest.o
cgdb dtest 
b je_malloc
r

http://www.web-lovers.com/c-jemalloc-address-problem.html

gdb_graphs

https://github.com/tarun27sh/gdb_graphs

  • ~/.gdbinit
set pagination off
set print pretty
set logging file ./test.log
set logging enabled on
  • test.c
#include<stdio.h>
#include<stdbool.h>
#include<stdlib.h>
#include<time.h>


static void func9(void) { printf("leaf\n"); }

void func8(void) { func9(); }

void func7_1(bool is_true, int *p) { printf("leaf (is_true=%d, ptr=%p\n", is_true, p); }
void func7(void) { func8(); }

void func6(void) { func7(); }

void func5_1(const char* str) { printf("leaf (%s)\n", str); }
void func5(void) {
    func5_1("graph me\n");
    func6();
}

void func4(void) { func5(); }
void func3_1(int a, int b, int c) { printf("leaf\n"); }
void func3(void) {
    func3_1(rand(), rand(), rand());
    func4();
}

void func2_1(int a, int b) { printf("leaf (%d, %d)\n", a, b);}
void func2(void) {
    func2_1(rand(),rand());
    func3();
}
void func1(void) { func2(); }
int main()
{
    srand(time(NULL));
    for(int i=0; i<10; ++i) {
            func1();
    }
        return 0;
}
gcc -g test.c -o test
gdb ./test

(gdb) rbreak test.c:.

Breakpoint 1 at 0x13a9: file test.c, line 37.
void func1(void);
Breakpoint 2 at 0x137a: file test.c, line 33.
void func2(void);
Breakpoint 3 at 0x134c: file test.c, line 32.
void func2_1(int, int);
Breakpoint 4 at 0x1316: file test.c, line 27.
void func3(void);
Breakpoint 5 at 0x12f2: file test.c, line 26.
void func3_1(int, int, int);
Breakpoint 6 at 0x12e2: file test.c, line 25.
void func4(void);
Breakpoint 7 at 0x12c6: file test.c, line 20.
void func5(void);
Breakpoint 8 at 0x129b: file test.c, line 19.
void func5_1(const char *);
Breakpoint 9 at 0x128b: file test.c, line 17.
void func6(void);
Breakpoint 10 at 0x1243: file test.c, line 11.
void func7(void);
Breakpoint 11 at 0x1210: file test.c, line 10.
void func7_1(_Bool, int *);
Breakpoint 12 at 0x1200: file test.c, line 8.
void func8(void);
Breakpoint 13 at 0x13b9: file test.c, line 39.
int main();
Breakpoint 14 at 0x11e9: file test.c, line 7.
static void func9(void);


(gdb) commands
Type commands for breakpoint(s) 1-14, one per line.
End with a line saying just "end".
>bt
>c
>end
(gdb) r


Breakpoint 1, func1 () at test.c:37
37	void func1(void) { func2(); }
#0  func1 () at test.c:37
#1  0x00005555555553e4 in main () at test.c:42

Breakpoint 2, func2 () at test.c:33
33	void func2(void) { 
#0  func2 () at test.c:33
#1  0x00005555555553b6 in func1 () at test.c:37
#2  0x00005555555553e4 in main () at test.c:42

Breakpoint 3, func2_1 (a=32767, b=-136364723) at test.c:32
32	void func2_1(int a, int b) { printf("leaf (%d, %d)\n", a, b);}
#0  func2_1 (a=32767, b=-136364723) at test.c:32
#1  0x000055555555539c in func2 () at test.c:34
#2  0x00005555555553b6 in func1 () at test.c:37
#3  0x00005555555553e4 in main () at test.c:42
leaf (1316343669, 1810645944)
...

[Inferior 1 (process 672172) exited normally]

Once the program finishes, it would have dumped the logs to the disk in `test.log` 
sudo apt-get install graphviz
pip install -r requiments.txt
python gen_graph.py -i test/test.log 
03/22/2023 08:33:26 PM [1] processing gdb bt data
03/22/2023 08:33:26 PM [2] adding nodes, edges, #ofnodes=14
03/22/2023 08:33:27 PM [3] Embedding JS
03/22/2023 08:33:27 PM [4] saving graph to:
03/22/2023 08:33:27 PM       /media/shihyu/ssd1/github/jason_note/src/gdb/src/gdb_graphs/test.svg

03/22/2023 08:33:27 PM Finished

Rust

  • test.rs
trait Printable {
    fn print(&self);
}

struct Point {
    x: i32,
    y: i32,
}

impl Printable for Point {
    fn print(&self) {
        println!("({}, {})", self.x, self.y);
    }
}

fn print_all<T: Printable>(list: Vec<T>) {
    for item in list {
        item.print();
    }
}

fn main() {
    let list = vec![Point { x: 1, y: 2 }, Point { x: 3, y: 4 }];
    print_all(list);
}
rustc -C debuginfo=2 test.rs
https://github.com/zupzup/rust-gdb-example

RISC-V Linux kernel debug 環境搭建

https://blog.csdn.net/m0_43422086/article/details/125276723

一、目的

搭建qemu-gdb risc-v64 linux kernel的調試環境。

二、準備工作

​ Build Ninja 和riscv-toolchain

​ 首先安裝必要的庫(這是編譯riscv toolchain必須安裝的庫文件)

sudo apt update 
sudo apt upgrade 
sudo apt install \
    git \
    autoconf \
    automake \
    autotools-dev \
    ninja-build \
    build-essential \
    libmpc-dev \
    libmpfr-dev \
    libgmp-dev \
    libglib2.0-dev \
    libpixman-1-dev \
    libncurses5-dev \
    libtool \
    libexpat-dev \
    zlib1g-dev \
    curl \
    gawk \
    bison \
    flex \
    texinfo \
    gperf \
    patchutils \
    bc


mkdir qemu-gdb-risc-v64 && qemu-gdb-risc-v64

①Build Ninja

git clone https://github.com/ninja-build/ninja.git
cd ninja
cmake -Bbuild-cmake
cmake --build build-cmake

然後在.bashrc中添加ninja/build-cmake目錄

編輯.bashrc如下:

export PATH=$PATH:/home/kali/Desktop/riscv-debug/ninja/build-cmake

②Build riscv-gnu-compiler toolchain and debug gdb

wget https://static.dev.sifive.com/dev-tools/freedom-tools/v2020.12/riscv64-unknown-elf-toolchain-10.2.0-2020.12.8-x86_64-linux-ubuntu14.tar.gz
tar -xzvf riscv64-unknown-elf-toolchain-10.2.0-2020.12.8-x86_64-linux-ubuntu14.tar.gz
mv riscv64-unknown-elf-toolchain-10.2.0-2020.12.8-x86_64-linux-ubuntu14  riscv64-unknown-elf-toolchain

接著編輯~/.bashrc,加入下面的環境變量:

export PATH=$PATH:/home/kali/Desktop/riscv-debug/riscv64-unknown-elf-toolchain/bin

③命令行安裝gcc-riscv64-linux-gnu-

sudo apt install binutils-riscv64-linux-gnu 
sudo apt install gcc-riscv64-linux-gnu

OR

自己下載工具鏈原始碼進行編譯容易配錯選項,我們使用編譯好的工具鏈即可。 網址:https://toolchains.bootlin.com 這個網站提供了一些已經編譯好的工具鏈,我們從中下載即可。 arch選擇riscv64-lp64d,libc選擇glibc,然後點選下載。 stable是穩定版,bleeding-edge是最新的,可根據需要選擇,這裡我們選擇bleeding-edge。

export PATH=xxxxxxxxxxxxxxxxxxxx/toolchain/riscv64-lp64d--glibc--bleeding-edge-2022.08-1/bin:$PATH

三、Build Qemu

git clone https://gitlab.com/qemu-project/qemu.git
cd qemu
git submodule init
git submodule update --recursive
mkdir build && cd build
../configure --prefix=/home/shihyu/.mybin/qemu
make



linux-user/ioctls.h:188:1: error: ‘SNDCTL_DSP_MAPINBUF’ undeclared here (not in a function)
linux-user/ioctls.h:189:1: error: ‘SNDCTL_DSP_MAPOUTBUF’ undeclared here (not in a function)
linux-user/ioctls.h:244:1: error: ‘SOUND_MIXER_ACCESS’ undeclared here (not in a function)


Replace this line :
#include <linux/soundcard.h>
by :
#include <linux/soundcard.h.oss3>
in the linux-user/syscall.c file.

四、Build opensbi

git clone https://github.com/riscv-software-src/opensbi.git
cd opensbi/
make CROSS_COMPILE=riscv64-linux-gnu- PLATFORM=generic

五、Build Busybox

wget https://busybox.net/downloads/busybox-1.35.0.tar.bz2
tar -jxvf busybox-1.35.0.tar.bz2
cd busybox-1.35.0/
make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- defconfig
make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- menuconfig
vim .config 

在.config中添加這句:

CONFIG_STATIC=y

添加完成

make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- -j $(nproc)
make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- install
cd _install 
mkdir proc sys dev etc etc/init.d
touch etc/init.d/rcS
vim etc/init.d/rcS

後保存回到busybox-1.35.0目錄

在rcS中添加以下內容:

#!bin/sh 
mount -t proc none /proc 
mount -t sysfs none /sys 
/sbin/mdev -s

添加後保存

接著執行下面兩條指令,這兩條指令需要root權限:

sudo mknod dev/console c 5 1 
sudo mknod dev/ram b 1 0

給rcS文件設置可執行屬性:

chmod 777 etc/init.d/rcS
find -print0 | cpio -0oH newc | gzip -9 > ../rootfs.img 

到此busybox操作完成。

六、Build Linux Kernel

wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.9.tar.xz
tar -xvf linux-5.9.tar.xz
cd linux-5.9

在內核Makefile的KBUILD_CFLAGS上添加-g選項,然後再執行下面命令:

make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- defconfig 
make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- -j $(nproc)

以上步驟完成後使用gdb調試qemu啟動linux kernel,qemu命令行如下:

qemu-system-riscv64 \
        -nographic -machine virt \
        -bios  opensbi/build/platform/generic/firmware/fw_dynamic.bin \
        -kernel linux-5.9/arch/riscv/boot/Image \
        -initrd busybox-1.35.0/rootfs.img  \
        -append "root=/dev/ram rdinit=/sbin/init nokaslr" \
        -S \
        -s

nokaslr 的核心參數是停用隨機分配 kernel 運作位址的功能

開啟另一個終端,進入剛剛的linux kernel 目錄(該目錄下有vmlinux文件),使用下面命令啟動gdb:

riscv64-unknown-elf-gdb vmlinux -ex 'target remote localhost:1234'
(gdb) b start_kernel 
Breakpoint 1 at 0xffffffe00000272e
(gdb) continue
Continuing.
Breakpoint 1, 0xffffffe00000272e in start_kernel ()

Go 和 Rust GDB 調試指南

Go 測試範例

程式碼 (main.go)

package main

import (
	"fmt"
	"time"
)

func add(a, b int) int {
	result := a + b
	fmt.Printf("Adding %d + %d = %d\n", a, b, result)
	return result
}

func factorial(n int) int {
	if n <= 1 {
		return 1
	}
	return n * factorial(n-1)
}

func main() {
	fmt.Println("=== Go GDB Debug Test ===")
	
	// 簡單變數
	x := 10
	y := 20
	
	// 函數呼叫
	sum := add(x, y)
	
	// 遞迴函數
	fact := factorial(5)
	
	// 陣列操作
	numbers := []int{1, 2, 3, 4, 5}
	total := 0
	for i, num := range numbers {
		total += num
		fmt.Printf("Index %d: %d, Running total: %d\n", i, num, total)
	}
	
	// 結構體
	type Person struct {
		Name string
		Age  int
	}
	
	person := Person{Name: "Alice", Age: 30}
	fmt.Printf("Person: %+v\n", person)
	
	// 故意的延遲,方便設置斷點
	time.Sleep(100 * time.Millisecond)
	
	fmt.Printf("Final results: sum=%d, factorial=%d, total=%d\n", sum, fact, total)
}

編譯和調試指令

# 編譯 (關閉優化)
go build -gcflags="-N -l" -o go_debug main.go

# 使用 GDB
gdb ./go_debug

# GDB 指令範例:
(gdb) break main.main
(gdb) run
(gdb) next
(gdb) print x
(gdb) print sum
(gdb) continue

Rust 測試範例

建立專案

# 建立 Cargo 專案
cargo new rust_debug_test
cd rust_debug_test

程式碼 (src/main.rs)

fn add(a: i32, b: i32) -> i32 {
    let result = a + b;
    println!("Adding {} + {} = {}", a, b, result);
    result
}

fn factorial(n: i32) -> i32 {
    if n <= 1 {
        1
    } else {
        n * factorial(n - 1)
    }
}

#[derive(Debug)]
struct Person {
    name: String,
    age: u32,
}

fn main() {
    println!("=== Rust GDB Debug Test ===");
    
    // 簡單變數
    let x = 10;
    let y = 20;
    
    // 函數呼叫
    let sum = add(x, y);
    
    // 遞迴函數
    let fact = factorial(5);
    
    // 向量操作
    let numbers = vec![1, 2, 3, 4, 5];
    let mut total = 0;
    
    for (i, num) in numbers.iter().enumerate() {
        total += num;
        println!("Index {}: {}, Running total: {}", i, num, total);
    }
    
    // 結構體
    let person = Person {
        name: String::from("Alice"),
        age: 30,
    };
    println!("Person: {:?}", person);
    
    // 選項類型
    let maybe_number: Option<i32> = Some(42);
    match maybe_number {
        Some(n) => println!("Found number: {}", n),
        None => println!("No number found"),
    }
    
    // 故意的延遲,方便設置斷點
    std::thread::sleep(std::time::Duration::from_millis(100));
    
    println!("Final results: sum={}, factorial={}, total={}", sum, fact, total);
}

編譯和調試指令

# 編譯
cargo build

# 使用 rust-gdb (推薦)
rust-gdb ./target/debug/rust_debug_test

# 或使用一般 GDB
gdb ./target/debug/rust_debug_test

# GDB 指令範例:
(gdb) break main
(gdb) run
(gdb) next
(gdb) print x
(gdb) print sum
(gdb) info locals
(gdb) continue

實用的 GDB 指令

設置斷點

break function_name           # 在函數入口設置斷點
break file.rs:line_number     # 在指定行設置斷點
break main.go:25              # 在指定檔案的指定行設置斷點
info breakpoints              # 顯示所有斷點
delete 1                      # 刪除編號 1 的斷點

執行控制

run                          # 開始執行程式
continue                     # 繼續執行直到下一個斷點
next                         # 執行下一行 (不進入函數)
step                         # 執行下一行 (進入函數)
finish                       # 執行完當前函數並返回
quit                         # 退出 GDB

查看變數和狀態

print variable_name          # 顯示變數值
print *pointer              # 顯示指標指向的值
info locals                 # 顯示所有局部變數
info args                   # 顯示函數參數
whatis variable_name        # 顯示變數類型

查看堆疊和框架

backtrace                   # 顯示調用堆疊
bt                          # backtrace 的縮寫
frame n                     # 切換到第 n 個堆疊框架
up                          # 上移一個框架
down                        # 下移一個框架

查看程式碼

list                        # 顯示當前位置的程式碼
list function_name          # 顯示指定函數的程式碼
list file.rs:20             # 顯示指定檔案的指定行

調試技巧

Go 特定注意事項

  • 使用 -gcflags="-N -l" 關閉優化,否則變數可能被優化掉
  • Go 的 goroutine 調試需要特殊處理
  • 某些 Go 內建類型在 GDB 中可能顯示不完整

Rust 特定注意事項

  • 優先使用 rust-gdb 而非一般 GDB,它有更好的 Rust 支援
  • Rust 的 OptionResult 類型在 GDB 中可能需要特殊處理
  • 使用 cargo build 而非 cargo build --release 以保留調試資訊

通用技巧

  • 在關鍵位置添加 sleeppause 來方便設置斷點
  • 使用 info locals 快速查看所有局部變數
  • 善用 backtrace 了解函數調用關係

GDB函數調用軌跡分析與流程圖生成完整指南

本指南介紹如何使用 GDB 記錄程式執行過程中的函數調用軌跡,並將其轉換為視覺化流程圖,適用於程式分析、調試和文檔製作。

方法一:使用 GDB 腳本記錄函數調用

1. 創建 GDB 追蹤腳本

# trace_functions.gdb
set logging file function_trace.txt
set logging on

# 設置在每個函數入口處的斷點
define trace_function
    if $argc == 1
        break $arg0
        commands
            silent
            printf "ENTER: %s at %s:%d\n", $arg0, __FILE__, __LINE__
            bt 1
            continue
        end
    end
end

# 設置在函數退出處的斷點
define trace_return
    if $argc == 1
        break $arg0
        commands
            silent
            printf "EXIT: %s\n", $arg0
            continue
        end
    end
end

# 追蹤特定函數
trace_function main.main
trace_function your_target_function

# 開始執行
run

2. 使用腳本運行

gdb -x trace_functions.gdb ./your_program

方法二:使用 GDB Python 腳本自動化

1. Python 腳本記錄調用軌跡

# function_tracer.py
import gdb
import json
from datetime import datetime

class FunctionTracer:
    def __init__(self):
        self.call_stack = []
        self.function_calls = []
        self.depth = 0
    
    def trace_function_entry(self, function_name):
        timestamp = datetime.now().isoformat()
        call_info = {
            'type': 'enter',
            'function': function_name,
            'depth': self.depth,
            'timestamp': timestamp,
            'stack': list(self.call_stack)
        }
        self.function_calls.append(call_info)
        self.call_stack.append(function_name)
        self.depth += 1
        print(f"{'  ' * (self.depth-1)}→ {function_name}")
    
    def trace_function_exit(self, function_name):
        timestamp = datetime.now().isoformat()
        if self.call_stack and self.call_stack[-1] == function_name:
            self.call_stack.pop()
            self.depth -= 1
        
        call_info = {
            'type': 'exit',
            'function': function_name,
            'depth': self.depth,
            'timestamp': timestamp
        }
        self.function_calls.append(call_info)
        print(f"{'  ' * self.depth}← {function_name}")
    
    def save_trace(self, filename):
        with open(filename, 'w') as f:
            json.dump(self.function_calls, f, indent=2)

# 創建全局追蹤器
tracer = FunctionTracer()

class FunctionBreakpoint(gdb.Breakpoint):
    def __init__(self, function_name, is_entry=True):
        super().__init__(function_name)
        self.function_name = function_name
        self.is_entry = is_entry
    
    def stop(self):
        if self.is_entry:
            tracer.trace_function_entry(self.function_name)
        else:
            tracer.trace_function_exit(self.function_name)
        return False  # 不停止執行

# 設置要追蹤的函數
functions_to_trace = [
    'main.main',
    'your_target_function',
    'another_function'
]

for func in functions_to_trace:
    FunctionBreakpoint(func, True)

# 執行完畢後保存
def save_and_exit():
    tracer.save_trace('function_trace.json')
    print("Trace saved to function_trace.json")

# 註冊退出處理
gdb.events.exited.connect(save_and_exit)

2. 使用 Python 腳本

gdb -x function_tracer.py ./your_program

方法三:使用外部工具 + GDB

1. 使用 ltracestrace 結合

# 記錄函數調用
ltrace -f -o function_calls.txt ./your_program

# 或者使用 strace 記錄系統調用
strace -f -o system_calls.txt ./your_program

2. 使用 perf 記錄調用圖

# 記錄調用圖
perf record -g ./your_program
perf report --stdio > call_graph.txt

# 生成調用圖
perf script | python /usr/share/perf-core/scripts/python/flamegraph.py > flamegraph.svg

方法四:轉換成流程圖

1. Python 腳本轉換 JSON 為 Mermaid

# json_to_mermaid.py
import json
import sys

def generate_mermaid_flowchart(trace_file):
    with open(trace_file, 'r') as f:
        calls = json.load(f)
    
    mermaid = ["graph TD"]
    
    # 提取函數調用關係
    call_stack = []
    edges = set()
    
    for call in calls:
        if call['type'] == 'enter':
            if call_stack:
                parent = call_stack[-1]
                child = call['function']
                edges.add((parent, child))
            call_stack.append(call['function'])
        elif call['type'] == 'exit' and call_stack:
            call_stack.pop()
    
    # 生成節點
    functions = set()
    for parent, child in edges:
        functions.add(parent)
        functions.add(child)
    
    # 清理函數名作為節點 ID
    def clean_name(name):
        return name.replace('.', '_').replace(':', '_')
    
    # 添加節點定義
    for func in functions:
        clean_func = clean_name(func)
        mermaid.append(f"    {clean_func}[\"{func}\"]")
    
    # 添加邊
    for parent, child in edges:
        clean_parent = clean_name(parent)
        clean_child = clean_name(child)
        mermaid.append(f"    {clean_parent} --> {clean_child}")
    
    return '\n'.join(mermaid)

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python json_to_mermaid.py function_trace.json")
        sys.exit(1)
    
    mermaid_code = generate_mermaid_flowchart(sys.argv[1])
    
    with open('function_flowchart.mmd', 'w') as f:
        f.write(mermaid_code)
    
    print("Mermaid flowchart saved to function_flowchart.mmd")
    print("\nMermaid code:")
    print(mermaid_code)

2. 轉換為 Graphviz DOT 格式

# json_to_dot.py
import json
import sys

def generate_dot_graph(trace_file):
    with open(trace_file, 'r') as f:
        calls = json.load(f)
    
    dot = ['digraph FunctionCalls {']
    dot.append('    rankdir=TD;')
    dot.append('    node [shape=box];')
    
    call_stack = []
    edges = set()
    
    for call in calls:
        if call['type'] == 'enter':
            if call_stack:
                parent = call_stack[-1]
                child = call['function']
                edges.add((parent, child))
            call_stack.append(call['function'])
        elif call['type'] == 'exit' and call_stack:
            call_stack.pop()
    
    # 添加邊
    for parent, child in edges:
        dot.append(f'    "{parent}" -> "{child}";')
    
    dot.append('}')
    return '\n'.join(dot)

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python json_to_dot.py function_trace.json")
        sys.exit(1)
    
    dot_code = generate_dot_graph(sys.argv[1])
    
    with open('function_graph.dot', 'w') as f:
        f.write(dot_code)
    
    print("DOT graph saved to function_graph.dot")
    print("Generate PNG with: dot -Tpng function_graph.dot -o function_graph.png")

方法五:使用 Valgrind Callgrind

1. 生成調用圖

# 使用 Valgrind 生成調用圖
valgrind --tool=callgrind ./your_program

# 查看調用圖
callgrind_annotate callgrind.out.*

# 生成可視化調用圖
gprof2dot -f callgrind callgrind.out.* | dot -Tpng -o callgraph.png

完整工作流程

1. 記錄函數調用

# 1. 編譯程式(保留 debug symbol)
go build -gcflags="-N -l" -o myprogram main.go

# 2. 使用 GDB Python 腳本記錄
gdb -x function_tracer.py ./myprogram

# 3. 或者使用簡單的 ltrace
ltrace -f -o calls.txt ./myprogram

2. 轉換為流程圖

# 轉換 JSON 為 Mermaid
python json_to_mermaid.py function_trace.json

# 或轉換為 DOT 格式
python json_to_dot.py function_trace.json
dot -Tpng function_graph.dot -o flowchart.png

3. 在線可視化

實用腳本:一鍵生成流程圖

#!/bin/bash
# generate_flowchart.sh

PROGRAM=$1
if [ -z "$PROGRAM" ]; then
    echo "Usage: $0 <program_path>"
    exit 1
fi

echo "🔍 分析程式: $PROGRAM"

# 1. 檢查 debug symbol
if ! file "$PROGRAM" | grep -q "not stripped"; then
    echo "❌ 程式沒有 debug symbol,請重新編譯"
    exit 1
fi

# 2. 使用 ltrace 記錄函數調用
echo "📊 記錄函數調用..."
timeout 30 ltrace -f -o function_calls.txt "$PROGRAM" 2>/dev/null

# 3. 解析並生成 DOT 格式
echo "🎨 生成流程圖..."
python3 -c "
import re
calls = []
with open('function_calls.txt', 'r') as f:
    for line in f:
        match = re.search(r'(\w+)\(', line)
        if match:
            calls.append(match.group(1))

# 生成簡單的調用關係
with open('flowchart.dot', 'w') as f:
    f.write('digraph G {\\n')
    f.write('  rankdir=TD;\\n')
    for i in range(len(calls)-1):
        f.write(f'  \"{calls[i]}\" -> \"{calls[i+1]}\";\\n')
    f.write('}\\n')
"

# 4. 生成 PNG
if command -v dot >/dev/null; then
    dot -Tpng flowchart.dot -o flowchart.png
    echo "✅ 流程圖已生成: flowchart.png"
else
    echo "✅ DOT 檔案已生成: flowchart.dot"
    echo "📝 安裝 graphviz 後執行: dot -Tpng flowchart.dot -o flowchart.png"
fi

總結

雖然 GDB 本身不能直接生成流程圖,但可以透過:

  1. 記錄函數調用軌跡 (GDB 腳本/Python/ltrace)
  2. 解析調用關係 (Python 腳本)
  3. 轉換為圖形格式 (Mermaid/Graphviz/D3.js)

這樣的組合可以有效地將程式執行流程可視化!

C++

c++性能測試工具:google benchmark入門

出處:https://www.cnblogs.com/apocelipes/p/11067594.html

sudo apt install g++ cmake
git clone https://github.com/google/benchmark.git
git clone https://github.com/google/googletest.git benchmark/googletest
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=RELEASE ../benchmark
make -j4
sudo make install
  • benchmark_example.cpp
#include <benchmark/benchmark.h>
#include <array>

constexpr int len = 6;

// constexpr function具有inline屬性,你應該把它放在標頭檔中
constexpr auto my_pow(const int i)
{
    return i * i;
}

// 使用operator[]讀取元素,依次存入1-6的平方
static void bench_array_operator(benchmark::State& state)
{
    std::array<int, len> arr;
    constexpr int i = 1;

    for (auto _ : state) {
        arr[0] = my_pow(i);
        arr[1] = my_pow(i + 1);
        arr[2] = my_pow(i + 2);
        arr[3] = my_pow(i + 3);
        arr[4] = my_pow(i + 4);
        arr[5] = my_pow(i + 5);
    }
}
BENCHMARK(bench_array_operator);

// 使用at()讀取元素,依次存入1-6的平方
static void bench_array_at(benchmark::State& state)
{
    std::array<int, len> arr;
    constexpr int i = 1;

    for (auto _ : state) {
        arr.at(0) = my_pow(i);
        arr.at(1) = my_pow(i + 1);
        arr.at(2) = my_pow(i + 2);
        arr.at(3) = my_pow(i + 3);
        arr.at(4) = my_pow(i + 4);
        arr.at(5) = my_pow(i + 5);
    }
}
BENCHMARK(bench_array_at);

// std::get<>(array)是一個constexpr function,它會返回容器內元素的引用,並在編譯期檢查陣列的索引是否正確
static void bench_array_get(benchmark::State& state)
{
    std::array<int, len> arr;
    constexpr int i = 1;

    for (auto _ : state) {
        std::get<0>(arr) = my_pow(i);
        std::get<1>(arr) = my_pow(i + 1);
        std::get<2>(arr) = my_pow(i + 2);
        std::get<3>(arr) = my_pow(i + 3);
        std::get<4>(arr) = my_pow(i + 4);
        std::get<5>(arr) = my_pow(i + 5);
    }
}
BENCHMARK(bench_array_get);

BENCHMARK_MAIN();

我們可以看到每一個benchmark測試用例都是一個類型為std::function<void(benchmark::State&)>的函數,其中benchmark::State&負責測試的運行及額外參數的傳遞。

隨後我們使用for (auto _: state) {}來運行需要測試的內容,state會選擇合適的次數來運行循環,時間的計算從循環內的語句開始,所以我們可以選擇像例子中一樣在for循環之外初始化測試環境,然後在循環體內編寫需要測試的程式碼。

測試用例編寫完成後我們需要使用BENCHMARK(<function_name>);將我們的測試用例註冊進benchmark,這樣程式執行時才會執行我們的測試。

最後是用BENCHMARK_MAIN();替代直接編寫的main函數,它會處理命令列參數並運行所有註冊過的測試用例生成測試結果。

示例中大量使用了constexpt,這是為了能在編譯期計算出需要的數值避免對測試產生太多噪音。

g++ -Wall -std=c++14 benchmark_example.cpp -pthread -lbenchmark

benchmark需要連結libbenchmark.so,所以需要指定-lbenchmark,此外還需要thread的支援,因為libstdc++不提供thread的底層實現,我們需要pthread。另外不建議使用-lpthread,官方表示會出現相容問題,在我這測試也會出現連結錯誤。注意檔案名稱一定要在-lbenchmark前面,否則編譯會失敗

測試結果與預期基本相符,std::get最快,at()最慢。

向測試用例傳遞參數

之前我們的測試用例都只接受一個benchmark::State&類型的參數,如果我們需要給測試用例傳遞額外的參數呢?

舉個例子,假如我們需要實現一個佇列,現在有ring buffer和linked list兩種實現可選,現在我們要測試兩種方案在不同情況下的性能表現:

// 必要的資料結構
#include "ring.h"
#include "linked_ring.h"
 
// ring buffer的測試
static void bench_array_ring_insert_int_10(benchmark::State& state)
{
    auto ring = ArrayRing<int>(10);
    for (auto _: state) {
        for (int i = 1; i <= 10; ++i) {
            ring.insert(i);
        }
        state.PauseTiming(); // 暫停計時
        ring.clear();
        state.ResumeTiming(); // 恢復計時
    }
}
BENCHMARK(bench_array_ring_insert_int_10);
 
// linked list的測試
static void bench_linked_queue_insert_int_10(benchmark::State &state)
{
    auto ring = LinkedRing<int>{};
    for (auto _:state) {
        for (int i = 0; i < 10; ++i) {
            ring.insert(i);
        }
        state.PauseTiming();
        ring.clear();
        state.ResumeTiming();
    }
}
BENCHMARK(bench_linked_queue_insert_int_10);
 
// 還有針對刪除的測試,以及針對string的測試,都是高度重複的程式碼,這裡不再羅列

很顯然,上面的測試除了被測試類型和插入的資料量之外沒有任何區別,如果可以通過傳入參數進行控制的話就可以少寫大量重複的程式碼。

編寫重複的程式碼是浪費時間,而且往往意味著你在做一件蠢事,google的工程師們當然早就注意到了這一點。雖然測試用例只能接受一個benchmark::State&類型的參數,但我們可以將參數傳遞給state對象,然後在測試用例中獲取:

static void bench_array_ring_insert_int(benchmark::State& state)
{
    auto length = state.range(0);
    auto ring = ArrayRing<int>(length);
    for (auto _: state) {
        for (int i = 1; i <= length; ++i) {
            ring.insert(i);
        }
        state.PauseTiming();
        ring.clear();
        state.ResumeTiming();
    }
}
BENCHMARK(bench_array_ring_insert_int)->Arg(10);

上面的例子展示瞭如何傳遞和獲取參數:

  1. 傳遞參數使用BENCHMARK宏生成的對象的Arg方法
  2. 傳遞進來的參數會被放入state對象內部儲存,通過range方法獲取,呼叫時的參數0是傳入參數的需要,對應第一個參數

Arg方法一次只能傳遞一個參數,那如果一次想要傳遞多個參數呢?也很簡單:

static void bench_array_ring_insert_int(benchmark::State& state)
{
    auto ring = ArrayRing<int>(state.range(0));
    for (auto _: state) {
        for (int i = 1; i <= state.range(1); ++i) {
            ring.insert(i);
        }
        state.PauseTiming();
        ring.clear();
        state.ResumeTiming();
    }
}
BENCHMARK(bench_array_ring_insert_int)->Args({10, 10});

上面的例子沒什麼實際意義,只是為了展示如何傳遞多個參數,Args方法接受一個vector對象,所以我們可以使用c++11提供的大括號初始化器簡化程式碼,獲取參數依然通過state.range方法,1對應傳遞進來的第二個參數。

有一點值得注意,參數傳遞只能接受整數,如果你希望使用其他類型的附加參數,就需要另外想些辦法了。

簡化多個類似測試用例的生成

測試用例傳遞參數的最終目的是為了在不編寫重複程式碼的情況下生成多個測試用例,在知道了如何傳遞參數後你可能會這麼寫:

static void bench_array_ring_insert_int(benchmark::State& state)
{
    auto length = state.range(0);
    auto ring = ArrayRing<int>(length);
    for (auto _: state) {
        for (int i = 1; i <= length; ++i) {
            ring.insert(i);
        }
        state.PauseTiming();
        ring.clear();
        state.ResumeTiming();
    }
}
// 下面我們生成測試插入10,100,1000次的測試用例
BENCHMARK(bench_array_ring_insert_int)->Arg(10);
BENCHMARK(bench_array_ring_insert_int)->Arg(100);
BENCHMARK(bench_array_ring_insert_int)->Arg(1000);

這裡我們生成了三個實例,會產生下面的結果:

看起來工作良好,是嗎?

沒錯,結果是正確的,但是記得我們前面說過的嗎——不要編寫重複的程式碼!是的,上面我們手動編寫了用例的生成,出現了可以避免的重複。

幸好ArgArgs會將我們的測試用例使用的參數進行註冊以便產生用例名/參數的新測試用例,並且返回一個指向BENCHMARK宏生成對象的指針,換句話說,如果我們想要生成僅僅是參數不同的多個測試的話,只需要鏈式呼叫ArgArgs即可:

BENCHMARK(bench_array_ring_insert_int)->Arg(10)->Arg(100)->Arg(1000);

結果和上面一樣。

但這還不是最優解,我們仍然重複呼叫了Arg方法,如果我們需要更多用例時就不得不又要做重複勞動了。

對此google benchmark也有解決辦法:我們可以使用Range方法來自動生成一定範圍內的參數。

先看看Range的原型:

BENCHMAEK(func)->Range(int64_t start, int64_t limit);

start表示參數範圍起始的值,limit表示範圍結束的值,Range所作用於的是一個_閉區間_。

但是如果我們這樣改寫程式碼,是會得到一個錯誤的測試結果:

BENCHMARK(bench_array_ring_insert_int)->Range(10, 1000);

error

為什麼會這樣呢?那是因為Range默認除了start和limit,中間的其餘參數都會是某一個基底(base)的冪,基地默認為8,所以我們會看到64和512,它們分別是8的平方和立方。

想要改變這一行為也很簡單,只要重新設定基底即可,通過使用RangeMultiplier方法:

BENCHMARK(bench_array_ring_insert_int)->RangeMultiplier(10)->Range(10, 1000);

現在結果恢復如初了。

使用Ranges可以處理多個參數的情況:

BENCHMARK(func)->RangeMultiplier(10)->Ranges({{10, 1000}, {128, 256}});

第一個範圍指定了測試用例的第一個傳入參數的範圍,而第二個範圍指定了第二個傳入參數可能的值(注意這裡不是範圍了)。

與下面的程式碼等價:

BENCHMARK(func)->Args({10, 128})
               ->Args({100, 128})
               ->Args({1000, 128})
               ->Args({10, 256})
               ->Args({100, 256})
               ->Args({1000, 256})

實際上就是用生成的第一個參數的範圍於後面指定內容的參數做了一個笛卡爾積。

使用參數生成器

如果我想定製沒有規律的更複雜的參數呢?這時就需要實現自訂的參數生成器了。

一個參數生成器的簽名如下:

void CustomArguments(benchmark::internal::Benchmark* b);

我們在生成器中計算處參數,然後呼叫benchmark::internal::Benchmark對象的Arg或Args方法像上兩節那樣傳入參數即可。

隨後我們使用Apply方法把生成器應用到測試用例上:

BENCHMARK(func)->Apply(CustomArguments);

其實這一過程的原理並不複雜,我做個簡單的解釋:

  1. BENCHMARK宏產生的就是一個benchmark::internal::Benchmark對象然後返回了它的指針
  2. benchmark::internal::Benchmark對象傳遞參數需要使用Arg和Args等方法
  3. Apply方法會將參數中的函數應用在自身
  4. 我們在生成器裡使用benchmark::internal::Benchmark對象的指針b的Args等方法傳遞參數,這時的b其實指向我們的測試用例

到此為止生成器是如何工作的已經一目瞭然了,當然從上面得出的結論,我們還可以讓Apply做更多的事情。

下面看下Apply的具體使用:

// 這次我們生成100,200,...,1000的測試用例,用range是無法生成這些參數的
static void custom_args(benchmark::internal::Benchmark* b)
{
    for (int i = 100; i <= 1000; i += 100) {
        b->Arg(i);
    }
}
 
BENCHMARK(bench_array_ring_insert_int)->RangeMultiplier(10)->Apply(custom_args);

自訂參數的測試結果:

custom_args

至此向測試用例傳遞參數的方法就全部介紹完了。

測量時間複雜度

google benchmark已經為我們提供了類似的功能,而且使用相當簡單。

具體的解釋在後面,我們先來看幾個例子,我們人為製造幾個時間複雜度分別為O(n), O(logn), O(n^n)的測試用例:

#include <benchmark/benchmark.h>

// 這裡都是為了演示而寫成的程式碼,沒有什麼實際意義
static void bench_N(benchmark::State& state)
{
    int n = 0;

    for ([[maybe_unused]] auto _ : state) {
        for (int i = 0; i < state.range(0); ++i) {
            benchmark::DoNotOptimize(n +=
                                         2); // 這個函數防止編譯器將表示式最佳化,會略微降低一些性能
        }
    }

    state.SetComplexityN(state.range(0));
}
BENCHMARK(bench_N)->RangeMultiplier(10)->Range(10, 1000000)->Complexity();

static void bench_LogN(benchmark::State& state)
{
    int n = 0;

    for ([[maybe_unused]] auto _ : state) {
        for (int i = 1; i < state.range(0); i *= 2) {
            benchmark::DoNotOptimize(n += 2);
        }
    }

    state.SetComplexityN(state.range(0));
}
BENCHMARK(bench_LogN)->RangeMultiplier(10)->Range(10, 1000000)->Complexity();

static void bench_Square(benchmark::State& state)
{
    int n = 0;
    auto len = state.range(0);

    for ([[maybe_unused]] auto _ : state) {
        for (int64_t i = 1; i < len * len; ++i) {
            benchmark::DoNotOptimize(n += 2);
        }
    }

    state.SetComplexityN(len);
}
BENCHMARK(bench_Square)->RangeMultiplier(10)->Range(10, 100000)->Complexity();

BENCHMARK_MAIN();

如何傳遞參數和生成批次測試我們在上一篇已經介紹過了,這裡不再重複。

需要關注的是新出現的state.SetComplexityNComplexity

首先是state.SetComplexityN,參數是一個64位整數,用來表示演算法總體需要處理的資料總量。benchmark會根據這個數值,再加上運行耗時以及state的迭代次數計算出一個用於後面預估平均時間複雜度的值。

Complexity會根據同一組的多個測試用例計算出一個較接近的平均時間複雜度和一個均方根值,需要和state.SetComplexityN配合使用。

Complexity還有一個參數,可以接受一個函數或是benchmark::BigO列舉,它的作用是提示benchmark該測試用例的時間複雜度,預設值為benchmark::oAuto,測試中會自動幫我們計算出時間複雜度。對於較為複雜的演算法,而我們又有預期的時間按複雜度,這時我們就可以將其傳給這個方法,比如對於第二個測試用例,我們還可以這樣寫:

static void bench_LogN(benchmark::State& state)
{
    // 中間部分與前面一樣,略過
}
BENCHMARK(bench_LogN)->RangeMultiplier(10)->Range(10, 1000000)->Complexity(benchmark::oLogN);

在選擇正確的提示後對測試結果幾乎沒有影響,除了偏差值可以降得更低,使結果更準確。

Complexity在計算時間複雜度時會保留複雜度的係數,因此,如果我們發現給出的提示的時間複雜度前的係數過大的話,就意味著我們的預估發生了較大的偏差,同時它還會計算出RMS值,同樣反應了時間複雜度的偏差情況。

運行我們的測試:

img

可以看到,自動的時間複雜度計算基本是精準的,可以在我們對演算法進行測試時提供一個有效的參考。


官方編譯

# Check out the library.
$ git clone https://github.com/google/benchmark.git
# Go to the library root directory
$ cd benchmark
# Make a build directory to place the build output.
$ cmake -E make_directory "build"
# Generate build system files with cmake, and download any dependencies.
$ cmake -E chdir "build" cmake -DBENCHMARK_DOWNLOAD_DEPENDENCIES=on -DCMAKE_BUILD_TYPE=Release ../
# or, starting with CMake 3.13, use a simpler form:
# cmake -DCMAKE_BUILD_TYPE=Release -S . -B "build"
# Build the library.
$ cmake --build "build" --config Release
Next, you can run the tests to check the build.
$ cmake -E chdir "build" ctest --build-config Release
If you want to install the library globally, also run:

sudo cmake --build "build" --config Release --target install
#include <benchmark/benchmark.h>

static void BM_SomeFunction(benchmark::State& state) {
  // Perform setup here
  for (auto _ : state) {
    // This code gets timed
    SomeFunction();
  }
}
// Register the function as a benchmark
BENCHMARK(BM_SomeFunction);
// Run the benchmark
BENCHMARK_MAIN();
$ g++ mybenchmark.cc -std=c++11 -isystem benchmark/include \
  -Lbenchmark/build/src -lbenchmark -lpthread -o mybenchmark

【硬核】乘以 0.01 和除以 100 哪個快?

在知乎上看到這個問題,覺得挺有趣的。下面的回答五花八門,但是就是沒有直接給出真正的benchmark結果的。還有直接搬反彙編程式碼的,只不過彙編裡用了 x87 FPU 指令集,天那這都 202x 年了真的還有程序用這個老掉牙的浮點運算指令集的嗎?

我也決定研究一下,讀反彙編,寫 benchmark。平台以 x86-64 為準,編譯器 clang 12,開編譯器最佳化(不開最佳化談速度無意義)

程式碼及反彙編

https://gcc.godbolt.org/z/rvT9nEE9Y

godbolt

簡單彙編語言科普

在深入反彙編之前,先要對彙編語言有簡單的瞭解。本文由於原始程式碼都很簡單,甚至沒有循環和判斷,所以涉及到的彙編指令也很少。

  1. 彙編語言與平台強相關,這裡以 x86-64(x86的64位相容指令集,由於被AMD最先發明,也稱作AMD64)為例,簡稱x64
  2. x64彙編語言也有兩種語法,一種為 Intel 語法(主要被微軟平台編譯器使用),一種為 AT&T 語法(是gcc相容編譯器的默認語法,但是gcc也支援輸出intel 語法)。個人感覺 Intel 語法更易懂,這裡以 Intel 語法為例
  3. 基本語法。例如 mov rcx, raxmov 是指令名“賦值”。rcxraxmov 指令的兩個運算元,他們都是通用暫存器名。Intel 彙編語法,第一個運算元同時被用於儲存運算結果。所以:
    1. mov rcx, rax,賦值指令,將暫存器 rax 中的值賦值給暫存器 rcx。翻譯為C語言程式碼為 rcx = rax
    2. add rcx, rax,加法指令,將暫存器 rcxrax 的值相加後,結果賦值給 rcx。翻譯為 C 語言程式碼為 rcx += rax
  4. 暫存器。編譯器最佳化後,多數操作都直接在暫存器中操作,不涉及記憶體訪問。下文只涉及三類暫存器(x64平台)。
    1. r 打頭的 rxx 是 64 位暫存器
    2. e 打頭的 exx 是 32 位暫存器,同時就是同名 64 位 rxx 暫存器的低 32 位部分。
    3. xmmX 是 128 位 SSE 暫存器。由於本文不涉及 SIMD 運算,可以簡單的將其當做浮點數暫存器。對於雙精度浮點數,只使用暫存器的低 64 位部分
  5. 呼叫約定。C語言特性,所有程式碼都依附於函數,呼叫函數時父函數向子函數傳值、子函數向父函數返回值的方式叫做函數呼叫約定。呼叫約定是保證應用程式 ABI 相容的最基本要求,不同的平台。不同的作業系統有不同的呼叫約定。本文的反彙編程式碼都是使用 godbolt 生成的,godbolt 使用的是 Linux 平台,所以遵循 Linux 平台通用的 System V 呼叫約定 呼叫約定。因為本文涉及到的程式碼都非常簡單(都只有一個函數參數),讀者只需要知道三點:
    1. 函數的第一個整數參數通過 rdi / edi 暫存器傳入(rdi / edi 存放呼叫方的第一個參數的值)。rdi 為 64 位暫存器,對應 long 類型(Linux 平台)。edi 為 32 位暫存器,對應 int 類型
    2. 函數的第一個浮點數參數通過 xmm0 暫存器傳入,不區分單、雙精度
    3. 函數的返回值整數類型通過 rax / eax 存放,浮點數通過 xmm0 存放

整數情況

整數除100
int int_div(int num) {
    return num / 100;
}

結果為

int_div(int):                            # @int_div(int)
        movsxd  rax, edi
        imul    rax, rax, 1374389535
        mov     rcx, rax
        shr     rcx, 63
        sar     rax, 37
        add     eax, ecx
        ret

稍作解釋。movsxd 為帶符號擴展賦值,可翻譯為 rax = (long)ediimul 為有符號整數乘法;shr 為邏輯右移(符號位補0);sar 為算數右移(符號位不變)

可以看到編譯器使用乘法和移位模擬除法運算,意味著編譯器認為這麼一大串指令也比除法指令快。程式碼裡一會算術右移一會邏輯右移是為了相容負數。如果指定為無符號數,結果會簡單一些

unsigned int_div_unsigned(unsigned num) {
    return num / 100;
}

結果為

int_div_unsigned(unsigned int):                  # @int_div_unsigned(unsigned int)
        mov     eax, edi
        imul    rax, rax, 1374389535
        shr     rax, 37
        ret

也可以強制讓編譯器生成除法指令,使用 volatile 大法

int int_div_force(int num) {
    volatile int den = 100;
    return num / den;
}

結果為

int_div_force(int):                     # @int_div_force(int)
        mov     eax, edi
        mov     dword ptr [rsp - 4], 100
        cdq
        idiv    dword ptr [rsp - 4]
        ret

稍作解釋。cdq(Convert Doubleword to Quadword)是有符號 32 位至 64 位整數轉化;idiv 是有符號整數除法。 整數除法指令使用比較複雜。首先運算元不能是立即數。然後如果除數是 32 位,被除數必須被轉化為 64 位,cdq 指令就是在做這個轉化(因為有符號位填充的問題)。另外彙編裡出現了記憶體操作 dword ptr [rsp - 4],這是 volatile 的負作用,會對結果有些影響。

整數乘0.01
int int_mul(int num) {
    return num * 0.01;
}

結果為

.LCPI3_0:
        .quad   0x3f847ae147ae147b              # double 0.01
int_mul(int):                            # @int_mul(int)
        cvtsi2sd        xmm0, edi
        mulsd   xmm0, qword ptr [rip + .LCPI3_0]
        cvttsd2si       eax, xmm0
        ret

稍作解釋。cvtsi2sd(ConVerT Single Integer TO Single Double)是整數到雙精度浮點數轉換,可翻譯為 xmm0 = (double) edimulsd 是雙精度浮點數乘法,cvttsd2si 是雙精度浮點數到整數轉換(截斷小數部分)。

因為沒有整數和浮點數運算的指令,實際運算中會先將整數轉換為浮點數,運算完畢後還要轉回來。電腦中整數和浮點數儲存方法不同,整數就是簡單的補碼,浮點數是 IEEE754 的科學計數法表示,這個轉換並不是簡單的位數補充。

浮點數的情況

浮點數除100
double double_div(double num) {
    return num / 100;
}

結果為

.LCPI4_0:
        .quad   0x4059000000000000              # double 100
double_div(double):                        # @double_div(double)
        divsd   xmm0, qword ptr [rip + .LCPI4_0]
        ret

稍作解釋。divsd是雙精度浮點數除法。因為 SSE 暫存器不能直接 mov 賦值立即數,立即數的運算元都是先放在記憶體中的,即 qword ptr [rip + .LCPI4_0]

浮點數乘0.01

double double_mul(double num) {
    return num * 0.01;
}

結果為

.LCPI5_0:
        .quad   0x3f847ae147ae147b              # double 0.01
double_mul(double):                        # @double_mul(double)
        mulsd   xmm0, qword ptr [rip + .LCPI5_0]
        ret

結果與除法非常接近,都只有一個指令,不需要解釋了

Benchmark

https://quick-bench.com/q/1rmqhuLLUyxRJNqSlcJfhubNGdU

結果

quick-bench

按照用時從小到大排序:

  1. 浮點數乘 100%
  2. 無符號整數除 150%
  3. 有符號整數除(編譯為乘法和移位) 200%
  4. 整數乘 220%
  5. 強制整數除 900%
  6. 浮點數除 1400%

分析

  1. 浮點數相乘只需要一條指令 mulsd,而且其指令延時只有 4~5 個週期,理論最快毫無疑問。
  2. 無符號整數除編譯為乘法、移位和賦值指令,整數乘法指令 imul 延時約 3~4 個週期,再加上移位和賦值,總用時比浮點數乘略高。
  3. 有符號整數除編譯之後指令個數比無符號版本略多,但多出來的移位、加法等指令都很輕量,所以用時很接近。
  4. 整數乘的用時居然和編譯為乘法的整數除十分接近我也很意外。整數、浮點數互轉指令 cvtsi2sdcvttsd2si 根據 CPU 型號不同有 3~7 的指令延時。當然 CPU 指令執行效率不能只看延時,還得考慮多指令並行的情況。但是這 3 條指令互相依賴,無法並行。
  5. 強制除法指令較慢符合期望。32 位整數除法指令 imul 延時約 10~11,如果為 64 位整數甚至高達 57。另外記憶體訪問(實際情況應該只涉及到快取記憶體)對速度也會有一些影響。
  6. 最慢的是浮點數除法。其指令 divsd 依據 CPU 型號不同有 14 ~ 20 延時,但是居然比有記憶體訪問的強制整數除法還慢有些意外。

本文中沒有測試單精度浮點數(float)的情況,因為默認情況下編譯器為了精度考慮會將 float 轉化為 double 計算,結果再轉回去,導致 float 運算比 double 還慢。這點可以使用 --ffast-math 避免,但是 quick-bench 沒有提供這個選項的組態。另外值得一提的是,如果啟用 --ffast-math 編譯參數,編譯器會把浮點數除編譯為浮點數乘

註:所有指令的延時資訊都可在此找到

https://www.agner.org/optimize/instruction_tables.pdf

auto_ptr、unique_ptr、shared_ptr

範本auto_ptr是C++98提供的解決方案,C++11已摒棄。

範本unique_ptr、shared_ptr是C++11提供的解決方案.

為什麼要摒棄auto_ptr呢?

先來看下面的賦值語句:

auto_ptr<string> ps(new string("I am a boy."));
auto_ptr<stirng> vocation;
vocation = ps;

上述賦值語句將完成什麼工作呢?如果ps和vocation是常規指針,則兩個指針將指向同一個string對象。這是不能接受的,因為程序將試圖刪除同一個對像兩次--一次是ps過期時,一次是vocation過期時。要避免這種問題,方法有多種。

  • 定義賦值運算子,使之執行深賦值。這樣兩個指針將指向不同的對象,其中的一個對像是另一個對象的副本。
  • 建立所有權(ownership)概念,對於特定的對象,只能有一個智能指針可擁有它,這樣只能擁有對象的智能指針的建構函式會刪除該對象。然後,讓賦值操作轉讓所有權。這就是用於auto_ptr和unique_ptr的策略,但unique_ptr的策略更嚴格。
  • 建立智能更高的指針,跟蹤引用特定對象的智能指針數。這稱為引用計數(reference counting)。例如,賦值時,計數將加1,而指針過期時,計數將減1。僅當最後一個指針過期時,才呼叫delete。這是shared_ptr採用的策略。

每種方法都有其用途,

1 下面是一個不適合使用auto_ptr的示例:

#include <iostream>
#include <string>
#include <memory>

using namespace std;

int main()
{
    auto_ptr<string> films[5] =
    {
        auto_ptr<string> (new string("one")),
        auto_ptr<string> (new string("two")),
        auto_ptr<string> (new string("three")),
        auto_ptr<string> (new string("four")),
        auto_ptr<string> (new string("five"))
    };
    auto_ptr<string> pwin;
    pwin = films[2];  // films[2] lose ownership

    cout << "films data is: " << endl;
    for(auto_ptr<string> s : films)
        cout << *s << endl;
    cout << "pwin: " << *pwin << endl;

    return 0;
}

下面是該程序的輸出:

films data is:
one
two

Process returned -1073741819 (0xC0000005)   execution time : 1.659 s
Press any key to continue.

錯誤的使用auto_ptr可能導致問題(這種程式碼的行為是不確定的,其行為可能隨系統而異)。這裡的問題在於,下面的語句將所有權從films[2]轉讓給pwin:

pwin = films[2];  // films[2] lose ownership

這導致films[2]不再引用該字串。在auto_ptr放棄對象的所有權後,邊可能使用它來訪問該對象。當程序列印films[2]指向的字串時,卻發現這是一個空指針,因此發生錯誤。

2 如果使用shared_ptr替換auto_ptr,則程序將正常運行。

示例程式碼:

#include <iostream>
#include <string>
#include <memory>

using namespace std;

int main()
{
    shared_ptr<string> films[5] =
    {
        shared_ptr<string> (new string("one")),
        shared_ptr<string> (new string("two")),
        shared_ptr<string> (new string("three")),
        shared_ptr<string> (new string("four")),
        shared_ptr<string> (new string("five"))
    };
    shared_ptr<string> pwin;
    pwin = films[2];

    cout << "films data is: " << endl;
    for(shared_ptr<string> s : films)
        cout << *s << endl;
    cout << "pwin: " << *pwin << endl;

    return 0;
}

其輸出如下:

films data is:
one
two
three
four
five
pwin: three

這次pwin和films[2]指向同一個對象,而引用計數從1增加到2。在程序末尾,後聲明的pwin首先呼叫其解構函式,該解構函式將引用計數降低到1。然後,shared_ptr陣列的成員被釋放,對films[2]呼叫解構函式時,將引用計數降低到0,並釋放以前分配的空間。

3 如果使用unique_ptr替換auto_ptr

#include <iostream>
#include <string>
#include <memory>

using namespace std;

int main()
{
    unique_ptr<string> films[5] =
    {
        unique_ptr<string> (new string("one")),
        unique_ptr<string> (new string("two")),
        unique_ptr<string> (new string("three")),
        unique_ptr<string> (new string("four")),
        unique_ptr<string> (new string("five"))
    };
    unique_ptr<string> pwin;
    pwin = films[2];

    cout << "films data is: " << endl;
    for(unique_ptr<string> s : films)
        cout << *s << endl;
    cout << "pwin: " << *pwin << endl;

    return 0;
}

則程序將在下述程式碼行出現編譯錯誤。

pwin = films[2];  // films[2] lose ownership

rvalue 參考

參考〉中談到,參考是物件的別名,在 C++ 中,「物件」這個名詞,不單只是指類別的實例,而是指記憶體中的一塊資料,那麼可以參考字面常量嗎?常量無法使用 & 取址,例如無法 &10,因此以下會編譯錯誤:

int &r = 10; // error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'

不過,加上 const 的話倒是可以:

const int &r = 10; 

常量是記憶體中臨時的資料,無法對常量取址,因此編譯器會將以上轉換為像是:

const int _n = 10;
const int &r = _n;

實際上,r 並不是真的參考至 10,而是 10 被複製給 _n,然後 r 參考至 _n,如果不加上 const,那麼你可能會以為變更了 r,就是變更了 10 位址處的值,因此就要求你一定得加上 const,不讓你改了。

為什麼會需要參考至常量?通常跟函式呼叫相關,這之後文件再來討論;類似地,以下會編譯失敗:

int a = 10;
int b = 20;
int &r = a + b; // error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'

這是因為 a + b 運算出的結果,會是在臨時的記憶體空間中,無法取址;類似地,若想通過編譯,必須加上 const

int a = 10;
int b = 20;
const int &r = a + b; 

不過在 C++ 11 之後,像以上的運算式,可以直接參考了:

int a = 10;
int b = 20;
int &&rr = a + b;

在以上的程式中,int&& 是 rvalue 參考(rvalue reference),rr 參考了 a + b 運算結果的空間,相對於以下的程式來說比較有效率:

int a = 10;
int b = 20;
int c = a + b; // 將 a + b 的結果複製給 c

因為不必有將值複製、儲存至 c 的動作,效率上比較好,特別是當 rvalue 運算式會產生龐大物件的時候,複製就會是個成本考量,例如 s1s2 若是個很長的 string,那麼 s1 + s2 的結果還會複製給目標 string 的話:

string result = s1 + s2;

改用以下會比較經濟:

string &&result = s1 + s2;

相對於 rvalue 參考,int& 這類參考就被稱為 lvalue 參考;只不過,lvalue 或 rvalue 是什麼?方才編譯錯誤的訊息中,似乎也出現了 lvalue、rvalue 之類的字眼,這些是什麼?

lvalue、rvalue 是 C++ 對運算式(expression)的分類方式,一個粗略的判別方式,是看看 & 可否對運算式取址,若可以的話,運算式是 lvalue,否則是個 rvalue。

若要精確的定義,可以參考〈Value categories〉,該文件中 History 的區段,有談到運算式分類的歷史,最早是從 CPL 開始對運算式區分為左側模式(left-hand mode)與右側模式(right-hand mode),左、右是指運算式是在指定的左或右側,有些運算式只有在指定的左側才會有意義。

C 語言有類似的分類方式,分為 lvalue 與其他運算式,l 似乎暗示著 left 的首字母,不過實際上,並非以指定的左、右來分類,lvalue 是指可以識別物件的運算式,白話點的說法是,運算式的結果會是個有名稱的物件

到了 C++ 98,非 lvalue 運算式被稱為 rvalue,一些 C 中非 lvalue 的運算式成了 lvalue,到了 C++ 11,運算式又被重新分類為〈Value categories〉中的結果。

許多文件取 lvalue、rvalue 的 l、r,將它們分別譯為左值、右值,就運算式的分類歷史來說,不能說是錯,不過嚴格來說,C++ 中 lvalue、rvalue 的 l、r,並沒有左、右的意思,lvalue、rvalue 只是個分類名稱。

在〈Value categories〉一開頭,可以看到目前的 C++ 標準,將運算式更細分為 glvalue、prvalue、xvalue、lvalue 與 rvalue,g 暗示為 generalized,pr 暗示為 pure,x 暗示為 eXpiring,就涵蓋關係而言,使用圖來表示會比較清楚:

rvalue 參考

具體來說,哪個運算式屬於哪個分類,〈Value categories〉都有舉例,當然,容易看到眼花花…

方才談到,一個粗略的判別方式,是看看 & 可否對運算式取址,若可以的話,運算式是 lvalue,否則是個 rvalue;另一個白話點的判別方式是,lvalue 運算式的結果會是個有名稱的物件,例如 a,rvalue 的結果是暫時性存在於記憶體,例如 a + b

那麼 ++ii++ 呢?在〈遞增、遞減、指定運算〉中談過,++i 運算結果是遞增後的 i,也就是 ++i 運算結果是個有名稱的物件,因此可以使用 lvalue 參考:

int i = 10;
int &r = ++i; // OK

然而 i++ 運算結果是遞增前的 i,暫時性存在於記憶體,若不指定給變數的話就不見了,因此 i++ 是個 rvalue,因此以下會編譯失敗:

int i = 10;
int &r = i++; // error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'

C++ 11 開始,若想參考 i++ 運算時暫時存在於記憶體中遞增前的 i,可以使用 rvalue 參考:

int i = 10;
int &&rr = i++; // OK

哪些是 lvalue,而哪些又是 rvalue,基本上還是以〈Value categories〉的定義為準,不清楚的話就查一下。

使用 rvalue 參考通常是為了效率上的考量,

還有個 std::move(定義於 utility 標頭檔)用來實現移動語義(move semantics),例如實現移動建構式(move constructor),這需要在認識類別定義、複製建構式等之後才能細談,就現階段而言,可以從 string 來稍微認識一下,例如,以下會將 s1 的資料複製給 s2

string s2 = s1;    // s1 是個 string,而這邊會複製 s1 的內容給 s2

s1 指定給 s2 後,就不再會用到原本的內容,那麼複製就是不必要的成本,若能把 s1 的內容直接移給 s2 的話就好了,C++ 11 開始可以這麼做:

string s2 = std::move(s1);

這麼一來,s1 的資料就被移至 s2 了,在這之後不能立即使用 s1 來取值,因為資料轉移出去了,取值結果是不可預期的,只能銷毀 s1,或者是重新指定字串給 s1

來看個簡單的示範:

#include <iostream> 
#include <string>
using namespace std; 

int main() { 
    string s1 = "abc";
    string s2 = s1;     //  複製 s1 的資料

    cout << s1 << endl; // 顯示 "abc"
    cout << s2 << endl; // 顯示 "abc"
} 

跟移動版本比較一下:

#include <iostream> 
#include <string>
#include <utility>

using namespace std; 

int main() { 
    string s1 = "abc";
    string s2 = std::move(s1);    //  轉移 s1 的資料

    // cout << s1 << endl;        // 這時取值結果不可預期
    cout << s2 << endl;           // 顯示 "abc"

    s1 = "xyz";                   // OK
    cout << s1 << endl;           // 這時可以取值
} 

移動版本之所以能夠運作,是因為 string 的建構式之一,使用了 rvalue 參考,而 std::move 的作用,其實是告訴編譯器,將指定的 lvalue 當成是 rvalue(某些程度就是一種 cast),以選擇定義了 rvalue 參考的建構式,而建構式中實現了移動來源資料的演算。

因為 move 這個名稱太平凡了,為了避免名稱衝突,建議包含 std 名稱空間,也就是使用 std::move


在Effective Modern C++中的一段對新C++特性的總結。其中rvalue references是一個比較核心的改進,對某些情況下對C++程式碼的效率很有幫助。最近在看相關的文件,筆者想寫篇關於rvalue references的介紹性文章;準備分兩部分:第一部分介紹下什麼是rvalue和rvalue references,第二部分介紹它的應用。

lvalue 和 rvalue

lvalue和rvalue的概念最初來自C語言,後來C++對它們有所擴展。最初,在C裡lvalue和rvalue貌似分別指一個賦值表示式的左邊和右邊值。(“L” stands for “left” and “R” stands for “right“)C++引入後,這個名字裡左邊啊,右邊啊,就變得不那麼清晰了;也就是C++裡它們不再侷限於賦值表示式的左邊和右邊了。 首先,有一點是肯定的,C++裡一個表示式要麼是lvalue的,要麼是rvalue。這裡有一點要強調,lvalue和rvalue的是表示式的屬性,不是object的屬性。(C++03 3.10/1 says: “Every expression is either an lvalue or an rvalue.”)

lvalue一般是有可以定址的儲存位置,它在表示式後還會存在(persist beyond a single expression)。rvalue一般是臨時性的,在表示式後就會消失;所以rvalue是無法得到地址的,原因是如果可以得到臨時東西的地址,那後續訪問這個地址將是災難性的。 還有一個判斷lvalue和rvalue的小竅門是試著對表示式取地址(&);能合法取地址的是lvalue,不能取的或者得到荒謬結果的是rvalue。比如,&x,&x[0]都是合理的,所以x和x[0]都是lvalue;而&7,&(x+1),&(x+y)都是非法的,所以7,(x+1),(x+y)都是rvalue。

lvalue 和 rvalue的例子

下面舉一些常見lvalue 和 rvalue的表示式。 以下是常見的lvalue:

int var = 0;
var = 1 + 2; // ok, var is an lvalue here
int* p1 = &var; // ok, var is an lvalue
obj, *ptr, ptr[index], ++x;   // lvalue

// function returned is rvalue, except it returns a reference
int x;
int& getRef ()
{
    return x;
}
getRef() = 4;     // lvalue, as getRef() returns a reference

常見的rvalue:

1 + 2;
var + 1 = 2 + 3; // error, var + 1 is an rvalue
int* p2 = &(var + 1); // error, var + 1 is an rvalue
x++;

// function returned is rvalue, except it returns a reference
int x;
int getVal ()
{
    return x;
}
getVal();    // rvalue
UserType().member_function(); // ok, calling a member function of the class rvalue

上面有兩點要注意的。第一是++x和x++。這兩哥們很像,平時幾乎沒區別(除了一個是先加再取x的值,一個是先取x的值後加)。但其實這倆是完全不同的表示式:前者是lvalue後者是rvalue!++x和x++都是增加x值,但是++x返回的是原來的x,++後x依然存在;而x++返回的只是一個x的臨時copy! 第二個要注意的是函數。只有返回引用時,函數才是lvalue;其他情況都是rvalue。

運算子多載中的 lvalue 和 rvalue問題

上面的例子中沒有涉及到運算子多載;其實運算子多載和函數是一樣的規則——只有返回引用時,運算子多載才是lvalue;其他情況都是rvalue。

reference operator[] (size_type n);

vector<int> v(10, 1729); 
v[0];  // is an lvalue because operator[]() return reference int& .

string operator+ (const string& lhs, const string& rhs);

string s(“foo”);
string t(“bar”);
s + t;   // is an rvalue because operator+() returns string (and &(s + t) is invalid).

string& operator= (const string& str);

s=t=p; // makes sense; as operator= is lvalue

lvalue 和 rvalue const屬性

lvalue 和 rvalue 都可以是const或non-const的。比如:

string one(“cute”);
const string two(“fluffy”);
string three() { return “kittens”; }
const string four() { return “are an essential part of a healthy diet”; }

one;     // modifiable lvalue
two;     // const lvalue
three(); // modifiable rvalue
four();  // const rvalue
const string&=three();

這裡最關鍵的是引用(Type &)的變化。引用bind到 lvaue上,可以用來觀察和修改變數值;所以非const引用不能作用於const lvaue和rvalue。作用於rvalue意味著可以修改臨時變數值,這是絕對禁止的。 const引用(const Type &)可以bind到任何value上,lvalues, const lvalues, rvalues, and const rvalues (and can be used to observe them).

C++ std::move

https://medium.com/@berton1679/c-std-move-133d99d87fc1

一開始接觸c++11/14的人而言,絕大多數都會對 std::move() 這個神奇的function 所困惑。首先讓我們直接看一下cppreference 中的介紹,

std::move is used to indicate that an object t may be "moved from", i.e. allowing the efficient transfer of resources from t to another object.In particular, std::move produces an xvalue expression that identifies its argument t. It is exactly equivalent to a static_cast to an rvalue reference type.

而其實 std::move() 並沒有移動任何的物件,基本上只是轉型而已,程式碼基本上是如下

static_cast<typename std::remove_reference<T>::type&&>(t)

所以他並不是什麼神奇的黑魔法,就只是轉型!

至於使用時機,我們用以下的class 作為例子方便說明

class BigObject
{
public:
    BigObject()
    {
        std::cout<<__PRETTY_FUNCTION__<<std::endl;
    }
    BigObject(int g)
    {
        std::cout<<__PRETTY_FUNCTION__<<std::endl;
        gg = g;
    }
    ~BigObject()
    {
        std::cout<<__PRETTY_FUNCTION__<<std::endl;
    }
    BigObject (const BigObject &b)
    {
        std::cout<<__PRETTY_FUNCTION__<<std::endl;
    }
    BigObject (BigObject &&b)
    {
        std::cout<<__PRETTY_FUNCTION__<<std::endl;
        this->gg = std::move(b.gg);
    }
    int gg = 0;
};

許多人一開始接觸 std::move() 誤以為可以增進效能,因為可以減少copy constructor 的次數,但這其實不一定正確的!!!!!

例如

BigObject test1(int i)
{
    auto dd = BigObject();
    if (i <= 0){
        dd.gg = 5;
        return dd;
    }
    dd.gg = i;
    return std::move(dd);
}
BigObject test2(int i)
{
    auto dd = BigObject();
    if (i <= 0){
        dd.gg = 5;
        return dd;
    }
    dd.gg = i;
    return dd;
}int main()
{
  auto tt1 = test1(5);
  auto tt2 = test2(5);  return 0;
}

output 則為

BigObject::BigObject()
BigObject::BigObject(BigObject&&)
BigObject::~BigObject()BigObject::BigObject()
BigObject::~BigObject()
BigObject::~BigObject()

很多人以為function return object 會多一個copy constructor 而使用 std::move() ,因為很多人以為overload 較低,但是很明顯看到 test2 卻比 test1 更有效率,因為沒有多餘的copy/move constructor, 因為compiler 會自動做 RVO(Return Value Optimization), 所以切記

function 裡面能使用RVO 就使用RVO 不要自作聰明使用 **std::move**

那麼到底什麼時候可以使用 std::move 讓程式加快呢?

可以參考以下例子

std::array<T> , std::vector<T> 基本上都可以支援random access 的container,但是 std::move 的實作卻差別很大

int main()
{
  std::vector<BigObject> test;
  test.resize(2);
  auto m_test = std::move(test);
  for(auto &it : test)
    std::cout<<it.gg<<std::endl;
  for(auto &it : m_test)
    std::cout<<it.gg<<std::endl;}

output

BigObject::BigObject()
BigObject::BigObject()
0
0
BigObject::~BigObject()
BigObject::~BigObject()

可以看到 std::vector 對應的move constructor 可以不會做多餘的constructor ,且原本的element 都移到 m_test 之中。

output

BigObject::BigObject()
BigObject::BigObject()
BigObject::BigObject(BigObject&&)
BigObject::BigObject(BigObject&&)
0
0
0
0
BigObject::~BigObject()
BigObject::~BigObject()
BigObject::~BigObject()
BigObject::~BigObject()

但是 std::array<T> 對應的 move constructor 卻有很大的分別,基本上是會對每個element 都 call move constructor 而不是對container

所以在使用 std::move() 語法的時候,最好要知道到底程式會怎麼跑,不然往往會自成程式碼的失控…..

最後分享一下,減少copy/move constructor 次數的確可以增進程式效能,但通常都是 演算法 > 程式優化 ,所以往往演算法都是優化的第一步,但是如果在特定產業的話,對程式的速度非常在意,那優化 c++ 程式邏輯的確可以增進效能,因為現在產業的關係常常做這類似的優化,之前就有利用 universal reference 降低constructor 次數增進約10%的效能。

第一篇先以 std::move() 開頭,之後可能會多講一下 c++ optimization 的心得,順便紀錄工作用到的能力


C++ 11引進了move semantic。在C++03時,”temporaries” or ”rvalues”都被視為non-modifiable,但C++11允許了右值的改動,因為這會有些時候相當有用。更精準來說:

當右值被初始化之後,即可以被更改。其註記方式為 T&&, for a type of T

而move semantics要解決的問題是:C++ 03中常常有不必要的copy,尤其在object pass by value的時候。

而move semantics是為了提升這部分的效能。

舉個例子:

假設現在有個 std::vector type(可以想像裡面有個 C-style array以及定義的size)。想像一個函數創造了這個vector並要將其回傳。按照C++03的方法則是宣告一個container來接收。如std::vector ret = f()。此時函數內部創造的temporary內容會被完整複製一份放到ret,接著從記憶體中抹去所有temporary memory。這個過程很繁瑣,因為複製與刪去這兩個步驟是不必要的。

再仔細看move constructor的運作方式。今天一個std::vector要透過rvalue形式的std::vector來創建,則會發生以下步驟。

  1. rvalue的vector中,C-style array pointer會被複製到destination vector中。
  2. 原本rvalue的C-array pointer會被指向null。
  3. 因為rvalue是temporaries,接下來的context也不會再使用到,所以其null pointer也不會被access(所以不用擔心out of scope之後,嘗試去delete一個null pointer的memory)。

由此可見,這裡完全摒棄了deep copy的過程,但仍是safe的狀態。

除了move constructor的case,還有在function return 一個新的物件的時候(舉例: std::vector),move semantics也可以避免多餘的deep copy。其原因是函數回傳一個新物件時,多了一步deep copy也是不必要的。在function return的這個case,temporary vector會自動被當作右值,move constructor會implicitly called。(在C++ 03中,如果沒有move constructor,則會自動呼叫copy constructor)

再來看一個stackoveflow的例子,幫助理解lvalue, rvalue以及move。

假設現在有個string class,同時也定義其copy constructor和destructor。

class string
{
char* datapublic:
    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

    ~string()
    {
        delete[] data;
    }
}

接著我們執行三種string的操作:

string a(x);
string b(x + y);
string c(// a function returning string);

這裡只有第一行的操作會使用到deep copy。這裡x代表的就是string這個object。一個實際存在記憶體中,透過x去reference的物件。這我們稱為lvalue

而第二與第三行都是在程式執行過程中產生的temporaries。我們沒辦法透過一個name去取得x + y或函數回傳的string object。這稱為rvalue。這些rvalues會在其存在的expression結束之後就被destroy。

而在接下來的部份我們要加入move constructor。我們可以透過rvalue reference &&偵測constructor的argument是否為rvalue。

因此我們定義:

string(string&& rhs)
{
    data = rhs.data;
    rhs.data = nullptr;
}

在move constructor中,我們可以對記憶體進行任何操作。只要最後右值是在一個valid state就可以。在這裡我們將rhs.data改成nullptr是為了避免rhs呼叫了destructor,刪除了被移動的string。

以上很容易看出來,move constructor做的事情是透過改變pointer,把source (rhs)的記憶體內容搬移到string中。

最後來看看assignment operator。當assignment接收的是lvalue時,呼叫的就會是copy constructor,若是rvalue,那就會是move constructor。如:

string c = a;
string c = a + b;

這裡move constructor做的事情只是更改了pointer指向的位置,而source object在之後也不可能被使用者操作,所以是個安全的操作。


一文讀懂C++右值引用和std::move

https://zhuanlan.zhihu.com/p/335994370?utm_id=0

C++11引入了右值引用,有一定的理解成本,工作中發現不少同事對右值引用理解不深,認為右值引用性能更高等等。本文從實用角度出發,用儘量通俗易懂的語言講清左右值引用的原理,性能分析及其應用場景,幫助大家在日常程式設計中用好右值引用和std::move。

1. 什麼是左值、右值

首先不考慮引用以減少幹擾,可以從2個角度判斷:左值可以取地址、位於等號左邊;而右值沒法取地址,位於等號右邊

int a = 5;
  • a可以通過 & 取地址,位於等號左邊,所以a是左值。
  • 5位於等號右邊,5沒法通過 & 取地址,所以5是個右值。

再舉個例子:

struct A {
    A(int a = 0) {
        a_ = a;
    }
 
    int a_;
};
 
A a = A();
  • 同樣的,a可以通過 & 取地址,位於等號左邊,所以a是左值。
  • A()是個臨時值,沒法通過 & 取地址,位於等號右邊,所以A()是個右值。

可見左右值的概念很清晰,有地址的變數就是左值,沒有地址的字面值、臨時值就是右值。

2. 什麼是左值引用、右值引用

引用本質是別名,可以通過引用修改變數的值,傳參時傳引用可以避免複製,其實現原理和指針類似。 個人認為,引用出現的本意是為了降低C語言指針的使用難度,但現在指針+左右值引用共同存在,反而大大增加了學習和理解成本。

2.1 左值引用

左值引用大家都很熟悉,能指向左值,不能指向右值的就是左值引用

int a = 5;
int &ref_a = a; // 左值引用指向左值,編譯通過
int &ref_a = 5; // 左值引用指向了右值,會編譯失敗

引用是變數的別名,由於右值沒有地址,沒法被修改,所以左值引用無法指向右值。

但是,const左值引用是可以指向右值的:

const int &ref_a = 5;  // 編譯通過

const左值引用不會修改指向值,因此可以指向右值,這也是為什麼要使用const &作為函數參數的原因之一,如std::vectorpush_back

void push_back (const value_type& val);

如果沒有constvec.push_back(5)這樣的程式碼就無法編譯通過了。

2.2 右值引用

再看下右值引用,右值引用的標誌是&&,顧名思義,右值引用專門為右值而生,可以指向右值,不能指向左值

int &&ref_a_right = 5; // ok
 
int a = 5;
int &&ref_a_left = a; // 編譯不過,右值引用不可以指向左值
 
ref_a_right = 6; // 右值引用的用途:可以修改右值

2.3 對左右值引用本質的討論

下邊的論述比較複雜,也是本文的核心,對理解這些概念非常重要。

2.3.1 右值引用有辦法指向左值嗎?

有辦法,std::move

int a = 5; // a是個左值
int &ref_a_left = a; // 左值引用指向左值
int &&ref_a_right = std::move(a); // 通過std::move將左值轉化為右值,可以被右值引用指向
 
cout << a; // 列印結果:5

在上邊的程式碼裡,看上去是左值a通過std::move移動到了右值ref_a_right中,那是不是a裡邊就沒有值了?並不是,列印出a的值仍然是5。

std::move是一個非常有迷惑性的函數,不理解左右值概念的人們往往以為它能把一個變數裡的內容移動到另一個變數,但事實上std::move移動不了什麼,唯一的功能是把左值強制轉化為右值,讓右值引用可以指向左值。其實現等同於一個類型轉換:static_cast<T&&>(lvalue)。 所以,單純的std::move(xxx)不會有性能提升,std::move的使用場景在第三章會講。

同樣的,右值引用能指向右值,本質上也是把右值提升為一個左值,並定義一個右值引用通過std::move指向該左值:

int &&ref_a = 5;
ref_a = 6; 
 
等同於以下程式碼:
 
int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6;

2.3.2 左值引用、右值引用本身是左值還是右值?

被聲明出來的左、右值引用都是左值。 因為被聲明出的左右值引用是有地址的,也位於等號左邊。仔細看下邊程式碼:

// 形參是個右值引用
void change(int&& right_value) {
    right_value = 8;
}
 
int main() {
    int a = 5; // a是個左值
    int &ref_a_left = a; // ref_a_left是個左值引用
    int &&ref_a_right = std::move(a); // ref_a_right是個右值引用
 
    change(a); // 編譯不過,a是左值,change參數要求右值
    change(ref_a_left); // 編譯不過,左值引用ref_a_left本身也是個左值
    change(ref_a_right); // 編譯不過,右值引用ref_a_right本身也是個左值
     
    change(std::move(a)); // 編譯通過
    change(std::move(ref_a_right)); // 編譯通過
    change(std::move(ref_a_left)); // 編譯通過
 
    change(5); // 當然可以直接接右值,編譯通過
     
    cout << &a << ' ';
    cout << &ref_a_left << ' ';
    cout << &ref_a_right;
    // 列印這三個左值的地址,都是一樣的
}

看完後你可能有個問題,std::move會返回一個右值引用int &&,它是左值還是右值呢? 從表示式int &&ref = std::move(a)來看,右值引用ref指向的必須是右值,所以move返回的int &&是個右值。所以右值引用既可能是左值,又可能是右值嗎? 確實如此:右值引用既可以是左值也可以是右值,如果有名稱則為左值,否則是右值

或者說:作為函數返回值的 && 是右值,直接聲明出來的 && 是左值。 這同樣也符闔第一章對左值,右值的判定方式:其實引用和普通變數是一樣的,int &&ref = std::move(a)int a = 5沒有什麼區別,等號左邊就是左值,右邊就是右值。

最後,從上述分析中我們得到如下結論:

  1. 從性能上講,左右值引用沒有區別,傳參使用左右值引用都可以避免複製。
  2. 右值引用可以直接指向右值,也可以通過std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
  3. 作為函數形參時,右值引用更靈活。雖然const左值引用也可以做到左右值都接受,但它無法修改,有一定侷限性。
void f(const int& n) {
    n += 1; // 編譯失敗,const左值引用不能修改指向變數
}

void f2(int && n) {
    n += 1; // ok
}

int main() {
    f(5);
    f2(5);
}

3. 右值引用和std::move的應用場景

按上文分析,std::move只是類型轉換工具,不會對性能有好處;右值引用在作為函數形參時更具靈活性,看上去還是挺雞肋的。他們有什麼實際應用場景嗎?

3.1 實現移動語義

在實際場景中,右值引用和std::move被廣泛用於在STL和自訂類中實現移動語義,避免複製,從而提升程序性能。 在沒有右值引用之前,一個簡單的陣列類通常實現如下,有建構函式複製建構函式賦值運算子多載解構函式等。深複製/淺複製在此不做講解。

class Array {
public:
    Array(int size) : size_(size) {
        data = new int[size_];
    }
     
    // 深複製構造
    Array(const Array& temp_array) {
        size_ = temp_array.size_;
        data_ = new int[size_];
        for (int i = 0; i < size_; i ++) {
            data_[i] = temp_array.data_[i];
        }
    }
     
    // 深複製賦值
    Array& operator=(const Array& temp_array) {
        delete[] data_;
 
        size_ = temp_array.size_;
        data_ = new int[size_];
        for (int i = 0; i < size_; i ++) {
            data_[i] = temp_array.data_[i];
        }
    }
 
    ~Array() {
        delete[] data_;
    }
 
public:
    int *data_;
    int size_;
};

該類的複製建構函式、賦值運算子多載函數已經通過使用左值引用傳參來避免一次多餘複製了,但是內部實現要深複製,無法避免。 這時,有人提出一個想法:是不是可以提供一個移動建構函式,把被複製者的資料移動過來,被複製者後邊就不要了,這樣就可以避免深複製了,如:

class Array {
public:
    Array(int size) : size_(size) {
        data = new int[size_];
    }
     
    // 深複製構造
    Array(const Array& temp_array) {
        ...
    }
     
    // 深複製賦值
    Array& operator=(const Array& temp_array) {
        ...
    }
 
    // 移動建構函式,可以淺複製
    Array(const Array& temp_array, bool move) {
        data_ = temp_array.data_;
        size_ = temp_array.size_;
        // 為防止temp_array析構時delete data,提前置空其data_      
        temp_array.data_ = nullptr;
    }
     
 
    ~Array() {
        delete [] data_;
    }
 
public:
    int *data_;
    int size_;
};

這麼做有2個問題:

  • 不優雅,表示移動語義還需要一個額外的參數(或者其他方式)。
  • 無法實現!temp_array是個const左值引用,無法被修改,所以temp_array.data_ = nullptr;這行會編譯不過。當然函數參數可以改成非const:Array(Array& temp_array, bool move){...},這樣也有問題,由於左值引用不能接右值,Array a = Array(Array(), true);這種呼叫方式就沒法用了。

可以發現左值引用真是用的很不爽,右值引用的出現解決了這個問題,在STL的很多容器中,都實現了以右值引用為參數移動建構函式移動賦值多載函數,或者其他函數,最常見的如std::vector的push_backemplace_back。參數為左值引用意味著複製,為右值引用意味著移動。

class Array {
public:
    ......
 
    // 優雅
    Array(Array&& temp_array) {
        data_ = temp_array.data_;
        size_ = temp_array.size_;
        // 為防止temp_array析構時delete data,提前置空其data_      
        temp_array.data_ = nullptr;
    }
     
 
public:
    int *data_;
    int size_;
};

如何使用:

// 例1:Array用法
int main(){
    Array a;
 
    // 做一些操作
    .....
     
    // 左值a,用std::move轉化為右值
    Array b(std::move(a));
}

3.2 實例:vector::push_back使用std::move提高性能

// 例2:std::vector和std::string的實際例子
int main() {
    std::string str1 = "aacasxs";
    std::vector<std::string> vec;
     
    vec.push_back(str1); // 傳統方法,copy
    vec.push_back(std::move(str1)); // 呼叫移動語義的push_back方法,避免複製,str1會失去原有值,變成空字串
    vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1會失去原有值
    vec.emplace_back("axcsddcas"); // 當然可以直接接右值
}
 
// std::vector方法定義
void push_back (const value_type& val);
void push_back (value_type&& val);
 
void emplace_back (Args&&... args);

在vector和string這個場景,加個std::move會呼叫到移動語義函數,避免了深複製。

除非設計不允許移動,STL類大都支援移動語義函數,即可移動的。 另外,編譯器會默認在使用者自訂的classstruct中生成移動語義函數,但前提是使用者沒有主動定義該類的複製構造等函數(具體規則自行百度哈)。 因此,可移動對像在<需要複製且被複製者之後不再被需要>的場景,建議使用std::move觸發移動語義,提升性能。

moveable_objecta = moveable_objectb; 
改為: 
moveable_objecta = std::move(moveable_objectb);

還有些STL類是move-only的,比如unique_ptr,這種類只有移動建構函式,因此只能移動(轉移內部對像所有權,或者叫淺複製),不能複製(深複製):

std::unique_ptr<A> ptr_a = std::make_unique<A>();

std::unique_ptr<A> ptr_b = std::move(ptr_a); // unique_ptr只有‘移動賦值多載函數‘,參數是&& ,只能接右值,因此必須用std::move轉換類型

std::unique_ptr<A> ptr_b = ptr_a; // 編譯不通過

std::move本身只做類型轉換,對性能無影響。 我們可以在自己的類中實現移動語義,避免深複製,充分利用右值引用和std::move的語言特性。

4. 完美轉發 std::forward

std::move一樣,它的兄弟std::forward也充滿了迷惑性,雖然名字含義是轉發,但他並不會做轉發,同樣也是做類型轉換.

與move相比,forward更強大,move只能轉出來右值,forward都可以。

std::forward(u)有兩個參數:T與 u。 a. 當T為左值引用類型時,u將被轉換為T類型的左值; b. 否則u將被轉換為T類型右值。

舉個例子,有main,A,B三個函數,呼叫關係為:main->A->B,建議先看懂2.3節對左右值引用本身是左值還是右值的討論再看這裡:

void B(int&& ref_r) {
    ref_r = 1;
}
 
// A、B的入參是右值引用
// 有名字的右值引用是左值,因此ref_r是左值
void A(int&& ref_r) {
    B(ref_r);  // 錯誤,B的入參是右值引用,需要接右值,ref_r是左值,編譯失敗
     
    B(std::move(ref_r)); // ok,std::move把左值轉為右值,編譯通過
    B(std::forward<int>(ref_r));  // ok,std::forward的T是int類型,屬於條件b,因此會把ref_r轉為右值
}
 
int main() {
    int a = 5;
    A(std::move(a));
}

例2:

void change2(int&& ref_r) {
    ref_r = 1;
}
 
void change3(int& ref_l) {
    ref_l = 1;
}
 
// change的入參是右值引用
// 有名字的右值引用是 左值,因此ref_r是左值
void change(int&& ref_r) {
    change2(ref_r);  // 錯誤,change2的入參是右值引用,需要接右值,ref_r是左值,編譯失敗
     
    change2(std::move(ref_r)); // ok,std::move把左值轉為右值,編譯通過
    change2(std::forward<int &&>(ref_r));  // ok,std::forward的T是右值引用類型(int &&),符合條件b,因此u(ref_r)會被轉換為右值,編譯通過
     
    change3(ref_r); // ok,change3的入參是左值引用,需要接左值,ref_r是左值,編譯通過
    change3(std::forward<int &>(ref_r)); // ok,std::forward的T是左值引用類型(int &),符合條件a,因此u(ref_r)會被轉換為左值,編譯通過
    // 可見,forward可以把值轉換為左值或者右值
}
 
int main() {
    int a = 5;
    change(std::move(a));
}

上邊的示例在日常程式設計中基本不會用到,std::forward最主要運於範本程式設計的參數轉發中,想深入瞭解需要學習萬能引用(T &&)引用摺疊(eg:& && → ?)等知識,本文就不詳細介紹這些了。

前言

私以為個人的技術水平應該是一個螺旋式上升的過程:先從書本去瞭解一個大概,然後在實踐中加深對相關知識的理解,遇到問題後再次回到書本,然後繼續實踐……接觸C++並行程式設計已經一年多,從慢慢啃《C++並行程式設計實戰》這本書開始,不停在期貨高頻交易軟體的開發實踐中去理解、運用、最佳化多執行緒相關技術。多執行緒知識的學習也是先從最基本的執行緒建立、互斥鎖、條件變數到更高級的執行緒安全資料結構、執行緒池等等技術,當然在項目中也用到了簡單的無鎖程式設計相關知識,今天把一些體會心得跟大家分享一下,如有錯誤,還望大家批評指正。

多執行緒並行讀寫

在編寫多執行緒程序時,最重要的問題就是多執行緒間共享資料的保護。多個執行緒之間共享地址空間,所以多個執行緒共享處理程序中的全域變數和堆,都可以對全域變數和堆上的資料進行讀寫,但是如果兩個執行緒同時修改同一個資料,可能造成某執行緒的修改丟失;如果一個執行緒寫的同時,另一個執行緒去讀該資料時可能會讀到寫了一半的資料。這些行為都是執行緒不安全的行為,會造成程式執行邏輯出現錯誤。舉個最簡單的例子:

#include <iostream>
#include <thread>

using namespace std;

int  i = 0;
mutex mut;

void iplusplus() {
    int c = 10000000;  //循環次數
    while (c--) {
        i++;
    }
}
int main()
{
    thread thread1(iplusplus);  //建立並運行執行緒1
    thread thread2(iplusplus);  //建立並運行執行緒2
    thread1.join();  // 等待執行緒1運行完畢
    thread2.join();  // 等待執行緒2運行完畢
    cout << "i = " << i << endl;
    return 0;
}

上面程式碼main函數中建立了兩個執行緒thread1和thread2,兩個執行緒都是運行iplusplus函數,該函數功能就是運行i++語句10000000次,按照常識,兩個執行緒各對i自增10000000次,最後i的結果應該是20000000,但是運行後結果卻是如下:

img

i並不等於20000000,這是在多執行緒讀寫情況下沒有對執行緒間共享的變數i進行保護所導致的問題。

有鎖程式設計

對於保護多執行緒共享資料,最常用也是最基本的方法就是使用C++11執行緒標準庫提供的互斥鎖mutex保護臨界區,保證同一時間只能有一個執行緒可以獲取鎖,持有鎖的執行緒可以對共享變數進行修改,修改完畢後釋放鎖,而不持有鎖的執行緒阻塞等待直到獲取到鎖,然後才能對共享變數進行修改,這種方法幾乎是並行程式設計中的標準做法。大體流程如下:

#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <chrono>

using namespace std;
int  i = 0;
mutex mut; //互斥鎖

void iplusplus() {
    int c = 10000000;  //循環次數
    while (c--) {
        mut.lock();  //互斥鎖加鎖
        i++;
        mut.unlock(); //互斥鎖解鎖
    }
}
int main()
{
    chrono::steady_clock::time_point start_time = chrono::steady_clock::now();//開始時間
    thread thread1(iplusplus);
    thread thread2(iplusplus);
    thread1.join();  // 等待執行緒1運行完畢
    thread2.join();  // 等待執行緒2運行完畢
    cout << "i = " << i << endl;
    chrono::steady_clock::time_point stop_time = chrono::steady_clock::now();//結束時間
    chrono::duration<double> time_span = chrono::duration_cast<chrono::microseconds>(stop_time - start_time);
    std::cout << "共耗時:" << time_span.count() << " ms" << endl; // 耗時
    system("pause");
    return 0;
}

程式碼14行和16行分別為互斥鎖加鎖和解鎖程式碼,29行我們列印程式執行耗時,程式碼運行結果如下:

img

可以看到,通過加互斥鎖,i的運行結果是正確的,由此解決了多執行緒同時寫一個資料產生的執行緒安全問題,程式碼總耗時3.37328ms。

無鎖程式設計

原子操作是無鎖程式設計的基石,原子操作是不可分隔的操作,一般通過CAS(Compare and Swap)操作實現,CAS操作需要輸入兩個數值,一個舊值(期望操作前的值)和一個新值,在操作期間先比較下舊值有沒有發生變化,如果沒有發生變化,才交換成新值,發生了變化則不交換。C++11的執行緒庫為我們提供了一系列原子類型,同時提供了相對應的原子操作,我們通過使用這些原子類型即可擺脫每次對共享變數進行操作都進行的加鎖解鎖動作,節省了系統開銷,同時避免了執行緒因阻塞而頻繁的切換。原子類型的基本使用方法如下:

#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <chrono>

using namespace std;
atomic<int> i = 0;

void iplusplus() {
    int c = 10000000;  //循環次數
    while (c--) {
        i++;
    }
}
int main()
{
    chrono::steady_clock::time_point start_time = chrono::steady_clock::now();//開始時間
    thread thread1(iplusplus);
    thread thread2(iplusplus);
    thread1.join();  // 等待執行緒1運行完畢
    thread2.join();  // 等待執行緒2運行完畢
    cout << "i = " << i << endl;
    chrono::steady_clock::time_point stop_time = chrono::steady_clock::now();//結束時間
    chrono::duration<double> time_span = chrono::duration_cast<chrono::microseconds>(stop_time - start_time);
    std::cout << "共耗時:" << time_span.count() << " ms" << endl; // 耗時
    system("pause");
    return 0;
}

程式碼的第8行定義了一個原子類型(int)變數i,在第13行多執行緒修改i的時候即可免去加鎖和解鎖的步驟,同時又能保證變數i的執行緒安全性。程式碼運行結果如下:

img

可以看到i的值是符合預期的,程式碼運行總耗時1.12731ms,僅為有鎖程式設計的耗時3.37328ms的1/3,由此可以看出無鎖程式設計由於避免了加鎖而相對於有鎖程式設計提高了一定的性能。

總結

無鎖程式設計最大的優勢是什麼?是性能提高嗎?其實並不是,我們的測試程式碼中臨界區非常短,只有一個語句,所以顯得加鎖解鎖操作對程序性能影響很大,但在實際應用中,我們的臨界區一般不會這麼短,臨界區越長,加鎖和解鎖操作的性能損耗越微小,無鎖程式設計和有鎖程式設計之間的性能差距也就越微小。

我認為無鎖程式設計最大的優勢在於兩點:

  1. 避免了死鎖的產生。由於無鎖程式設計避免了使用鎖,所以也就不會出現並行程式設計中最讓人頭疼的死鎖問題,對於提高程序健壯性有很大積極意義
  2. 程式碼更加清晰與簡潔。對於一個多執行緒共享的變數,保證其安全性我們只需在聲明時將其聲明為原子類型即可,在程式碼中使用的時候和使用一個普通變數一樣,而不用每次使用都要在前面寫個加鎖操作,在後面寫一個解鎖操作。我寫的C++期貨高頻交易軟體中,有一個全域變數fund,儲存的是當前資金量,程序採用執行緒池運行交易策略,交易策略中頻繁使用到fund變數,如果採用加鎖的方式,使用起來極其繁瑣,為了保護一個fund變數需要非常頻繁的加鎖解鎖,後來將fund變數改為原子類型,後面使用就不用再考慮加鎖問題,整個程序閱讀起來清晰很多。

如果是為了提高性能將程序大幅改寫成無鎖程式設計,一般來說結果可能會讓我們失望,而且無鎖程式設計裡面需要注意的地方也非常多,比如ABA問題,記憶體順序問題,正確實現無鎖程式設計比實現有鎖程式設計要困難很多,除非有必要(確定了性能瓶頸)才去考慮使用無鎖程式設計,否則還是使用互斥鎖更好,畢竟程序的高性能是建立在程序正確性的基礎上,如果程序不正確,一切性能提升都是徒勞無功。

C++ 為核心語言的高頻交易系統是如何做到低延遲?

出處:https://kknews.cc/tech/bozorm9.amp

問題中限定語言是C++,可討論的範圍就比較精簡了。現有的答案都在談系統架構層次上的東西,略顯跑題。我對C++瞭解不多,但我嘗試以一名C++程式設計師的視角,從基本思路出發做一個分析,拋磚引玉。

首先我們要明確係統的需求。所謂交易系統,從一個應用程式的角度來說,有以下幾個特點:

  1. 一定是一個網絡相關的應用,假如機器沒聯網,肯定什麼交易也幹不了。所以系統需要通過TCP/IP連接來收發數據。數據要分兩種,一種從交易所發過來的市場數據,流量很大,另一種是系統向交易所發出的交易指令,相比前者流量很小,這兩種數據需要在不同的TCP/IP連接裡傳輸。
  2. 因為是自動化交易系統,人工幹預的部分肯定比較小,所以圖形界面不是重點。而為了性能考慮,圖形界面需要和後臺分開部署在不同的機器上,通過網絡交互,以免任何圖形界面上的問題導致後臺系統故障或者被搶佔資源。這樣又要在後臺增加新的TCP/IP連接。
  3. 高頻交易系統對延遲異常敏感,目前(2014)市面上的主流系統(可以直接買到的大眾系統)延遲至少在100微秒級別,頂尖的系統(HFT專有)可以做到10微秒以下。其他答案裡提到C++隨便寫寫延遲做到幾百微秒,是肯定不行的,這樣的性能對於高頻交易來說會是一場災難。
  4. 系統只需要專注於處理自己收到的數據,不需要和其他機器合作,不需要擔心流量過載。

有了以上幾點基本的認識,我們可以看看用C++做為開發語言有哪些需要注意的。

首先前兩點需求就決定了,這種系統一定是一個多線程程序。雖然對於圖形界面來說,後臺系統相當於一個服務端,但這部分的性能不是重點,用常用的模式就能解決。而重要的面向交易所那端,系統其實是一個客戶端程序,只需要維護好固定數量的連接就可以了。為延遲考慮,一定要選擇異步I/O(阻塞的同步I/O會消耗時間在上下文切換),這裡有兩點需要注意:

  • 是否可以在單線程內完成所有處理?考慮市場數據的流量遠遠高於發出的交易指令,在單線程內處理顯然是不行的,否則可能收了一大堆數據還沒開始處理,錯過了發指令的最佳時機。
  • 有答案提到要壓低平時的資源使用率,這是完全錯誤的設計思路。問題同樣出在上下文切換上,一旦系統進入IDLE狀態,再重新切換回處理模式是要付出時間代價的。正確的做法是保持對異步socket的瘋狂輪詢,一旦有消息就立刻處理,之後繼續輪詢,這樣是最快的處理方式。(順帶一提現在的CPU一般會帶有環保功能,使用率低了會導致CPU進入低功耗模式,同樣對性能有嚴重影響。真正的低延遲系統一定是永遠發燙的!)

現在我們知道核心的模塊是一個多線程的,處理多個TCP/IP連接的模塊,接下來就可以針對C++進行討論。因為需要對接受到的每個TCP或UDP包進行處理,首先要考慮的是如何把包從接收線程傳遞給處理線程。我們知道C++是面向對象的語言,一般情況下最直觀的思路是創建一個對象,然後發給處理線程,這樣從邏輯上看是非常清晰的。但在追求低延遲的系統裡不能這樣做,因為對象是分配在堆上的,而堆的內存結構對我們來說是完全不透明的,沒辦法控制一個對象會具體分到內存的什麼位置上,這直接導致的問題是本來連續收到的網絡包,在內存裡的分佈是分散的,當處理線程需要讀取數據時就會發生大量的cache miss,產生不可控的延遲。所以對C++開發者來說,第一條需要謹記的應該是,不要隨便使用堆(用關鍵字new)。核心的數據要保證分配在連續內存裡。

另一個問題在於,市場數據和交易指令都是結構化的,包含了股票名稱,價格,時間等一系列信息。如果使用C++ class來對數據進行建模和封裝,同樣會產生不可知的內存結構。為了嚴格控制內存結構,應該使用struct來封裝。一方面在對接收到的數據解析時可以直接定義名稱,一方面在分配新對象(比如交易指令)時可以保證所有數據都分配在連續的內存區域。

以上兩點是關於延遲方面最重要的注意事項。除此之外,需要考慮的是業務邏輯的編寫。高頻交易系統裡註定了業務邏輯不會太複雜,但重要的是要保證正確性和避免指針錯誤。正確性應該可以藉助於C++的特性比如強類型,模板等來加強驗證,這方面我不熟悉就不多說了。高頻系統往往運行時要處理大量訂單,所以一定要保證系統運行時不能崩潰,一旦coredump後果很嚴重。這個問題也許可以多做編譯期靜態分析來加強,或者需要在系統外增加安全機制,這裡不展開討論了。

以下是幾點引申思考:

  • 如何存儲系統日誌?
  • 如何對系統進行實時監控?
  • 如果系統coredump,事後如何分析找出問題所在?
  • 如何設計保證系統可用性,使得出現coredump之類的情況時可以及時切換到備用系統?

這些問題相信在C++框架內都有合適的解決方案,我對此瞭解不多,所以只列在這裡供大家討論。


限制 CPU 核心和減少上下文切換(context switch)雖然相關,但它們是兩個不同的概念,目的是為了優化性能。

  1. 限制 CPU 核心(CPU Affinity) CPU 親和性(CPU Affinity) 是指將特定的線程或進程綁定到特定的 CPU 核心上運行。這樣做的好處包括:

減少延遲:透過將線程固定在特定核心上,系統能夠避免因為進程在核心之間切換而帶來的額外延遲。 資源利用:能夠更有效地利用 CPU 的快取(cache),因為當一個線程多次在同一核心上運行時,該核心的快取能夠保持相關數據,減少記憶體訪問的時間。 2. 減少上下文切換(Context Switch) 上下文切換 是指操作系統在不同的線程或進程之間切換的過程。這一過程涉及保存當前進程的狀態(上下文)並載入下一個進程的狀態。減少上下文切換的好處包括:

提高效率:每次上下文切換都需要時間和資源,因此減少上下文切換能夠提高 CPU 的有效利用率。 增強響應性:在高頻交易等時間敏感的應用中,減少上下文切換能夠確保系統在需要時迅速響應。 如何關聯 關聯性:限制 CPU 核心能夠幫助減少上下文切換的發生。例如,當一個線程被固定在特定的核心上運行時,操作系統就不需要頻繁地在不同核心之間切換這個線程,從而減少了上下文切換的次數。

獨立的考量:雖然這兩者可以互相促進性能,但仍然是獨立的優化策略。你可以選擇使用 CPU 親和性來優化某些關鍵任務的性能,同時也需要考慮整個系統的設計來減少上下文切換的發生。

總結 限制 CPU 核心是通過將線程綁定到特定核心來減少延遲和提高資源利用率。 減少上下文切換是通過降低線程或進程之間的切換次數來提高系統效率。 這兩者都在高頻交易等要求高性能的應用中發揮重要作用,但它們的實施策略和影響是不同的。如果你對這方面還有其他疑問,隨時告訴我!

在 C++ 中,如果你想要指定一個程式或一個線程在特定的 CPU 核心上運行,可以使用 POSIX threads(pthread)或 C++11 中的 库,結合系統調度工具(例如 sched_setaffinity)來實現。以下是使用這兩種方式的範例:

#include <iostream>
#include <pthread.h>
#include <sched.h>
#include <unistd.h>

void* threadFunction(void* arg) {
    // 這裡是線程要執行的代碼
    std::cout << "Thread running on CPU core: " << sched_getcpu() << std::endl;
    return nullptr;
}

int main() {
    pthread_t thread;
    

    // 創建一個新線程
    pthread_create(&thread, nullptr, threadFunction, nullptr);
    
    // 設定線程在第一個核心運行
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(0, &cpuset); // 指定使用 CPU 0
    
    pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
    
    // 等待線程結束
    pthread_join(thread, nullptr);
    
    return 0;

}
#include <iostream>
#include <thread>
#include <pthread.h>
#include <sched.h>
#include <unistd.h>

void threadFunction() {
    std::cout << "Thread running on CPU core: " << sched_getcpu() << std::endl;
}

int main() {
    std::thread myThread(threadFunction);
    

    // 設定線程在第一個核心運行
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(0, &cpuset); // 指定使用 CPU 0
    
    // 獲取 pthread 的線程 ID
    pthread_t nativeThread = myThread.native_handle();
    
    pthread_setaffinity_np(nativeThread, sizeof(cpu_set_t), &cpuset);
    
    // 等待線程結束
    myThread.join();
    
    return 0;

}

減少上下文切換(context switch)是提升系統性能的一個重要手段,尤其在高頻交易等需要低延遲的應用中。以下是一些有效的策略來減少上下文切換:

  1. 優化線程數量 合適的線程數量:確保線程數量與可用的 CPU 核心數量相匹配。過多的線程會導致頻繁的上下文切換,因為操作系統需要在更多的線程之間進行切換。

使用線程池:實現線程池來重用線程,減少線程創建和銷毀的開銷。

  1. 使用 CPU 親和性 CPU Affinity:將特定的線程綁定到特定的 CPU 核心上運行。這樣可以減少因線程在不同核心間切換而造成的上下文切換,並提高 CPU 快取的命中率。
  2. 降低 I/O 操作頻率 批量處理 I/O 操作:盡量將 I/O 操作合併處理,而不是頻繁地進行小的 I/O 操作。這樣可以減少因 I/O 等待而導致的上下文切換。

異步 I/O:使用異步 I/O 操作,讓線程在等待 I/O 完成時能夠執行其他任務,減少阻塞情況。

  1. 減少鎖的使用 無鎖編程:盡量使用無鎖資料結構,這樣可以減少因鎖而導致的上下文切換。

使用輕量級鎖:如果必須使用鎖,考慮使用輕量級鎖(如自旋鎖),這樣可以減少鎖的持有時間和上下文切換的開銷。

  1. 合理設計程式邏輯 合併任務:將多個小任務合併成較大的任務,以減少任務切換的頻率。

使用事件驅動架構:通過事件驅動的設計來減少線程的數量,這樣可以在一個線程中處理多個事件,降低上下文切換的頻率。

  1. 優化系統參數 調整操作系統參數:根據應用的特性調整操作系統的參數,如調整調度策略或提高線程優先級,從而減少上下文切換。
  2. 監控和分析 性能監控:使用工具(如 perf 或 top)來監控系統的上下文切換情況,識別高上下文切換的源頭並針對性優化。

性能分析:通過分析程式的執行路徑和資源使用,找出影響性能的瓶頸並進行針對性優化。

總結 減少上下文切換需要對系統架構、程式設計和資源管理進行綜合考量。透過上述策略,可以有效降低上下文切換的發生,從而提高系統的整體性能。如果你需要針對具體情況的建議,隨時告訴我!


在高頻交易中,由於交易策略需要極快的執行速度和低延遲,選擇適當的鎖機制非常重要。以下是一些常用的鎖定方法以及它們的適用情境:

  1. 互斥鎖 (Mutex)

    • 最常見的鎖,用於保護共享資源,確保同一時間只有一個執行緒可以訪問該資源。
    • 在高頻交易中,通常需要盡量減少鎖的持有時間,以降低延遲。
  2. 自旋鎖 (Spinlock)

    • 在鎖不可用的情況下,執行緒會進行忙等待(spin),而不是進入休眠狀態。
    • 適合於短時間的鎖持有,因為自旋鎖能夠避免上下文切換的開銷,但若鎖持有時間過長會造成CPU資源浪費。
  3. 讀寫鎖 (Read-Write Lock)

    • 允許多個執行緒同時讀取,但在寫入時會獲得獨佔鎖。
    • 對於讀取操作頻繁、寫入操作相對少的情況非常有效。
  4. 無鎖資料結構 (Lock-Free Data Structures)

    • 使用原子操作(如 CAS,Compare-And-Swap)來實現不需要鎖的資料結構。
    • 在高頻交易中,無鎖資料結構可以降低延遲,提高併發性能。
  5. 事件鎖 (Event Lock)

    • 允許執行緒等待特定事件的發生,適合於需要等待某個條件才能繼續執行的場景。
    • 在高頻交易中,可以用於處理異步事件,如市場報價更新。

小結:

在高頻交易中,通常會根據具體情況選擇適當的鎖定機制,並努力減少鎖的使用和鎖持有時間,以實現更高的執行效率和更低的延遲。無鎖資料結構的使用也越來越普遍,因為它們能夠顯著降低上下文切換和鎖競爭的影響。


在 Linux 中,使用 C 語言實現自旋鎖 (Spinlock) 通常依賴於原子操作,特別是使用 pthread 庫中的自旋鎖來實現多執行緒的同步。以下是使用 C 語言和 pthread 實現自旋鎖的範例。

使用 pthread 實現的 Spinlock 範例

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define NUM_THREADS 4
#define INCREMENTS 100000

pthread_spinlock_t spinlock; // 定義自旋鎖
int counter = 0; // 需要保護的共享資源

void* increment_counter(void* arg) {
    for (int i = 0; i < INCREMENTS; ++i) {
        pthread_spin_lock(&spinlock); // 獲取自旋鎖
        counter++; // 共享資源的操作
        pthread_spin_unlock(&spinlock); // 釋放自旋鎖
    }
    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];

    // 初始化自旋鎖
    if (pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE) != 0) {
        perror("Failed to initialize spinlock");
        return EXIT_FAILURE;
    }

    // 創建多個執行緒
    for (int i = 0; i < NUM_THREADS; ++i) {
        if (pthread_create(&threads[i], NULL, increment_counter, NULL) != 0) {
            perror("Failed to create thread");
            return EXIT_FAILURE;
        }
    }

    // 等待所有執行緒完成
    for (int i = 0; i < NUM_THREADS; ++i) {
        pthread_join(threads[i], NULL);
    }

    printf("Final counter value: %d\n", counter);

    // 銷毀自旋鎖
    pthread_spin_destroy(&spinlock);
    return EXIT_SUCCESS;
}

總結

  1. 自旋鎖的定義:我們使用 pthread_spinlock_t 來定義自旋鎖。
  2. 自旋鎖的初始化:使用 pthread_spin_init() 初始化自旋鎖。
  3. 獲取和釋放自旋鎖:使用 pthread_spin_lock()pthread_spin_unlock() 來獲取和釋放自旋鎖,確保對共享資源的安全訪問。
  4. 多執行緒創建:使用 pthread_create() 創建多個執行緒,並使用 pthread_join() 等待它們完成。
  5. 自旋鎖的銷毀:最後,使用 pthread_spin_destroy() 銷毀自旋鎖。

這個範例展示了如何在 Linux 環境中使用 C 語言實現自旋鎖來保護共享資源,並在多執行緒環境中進行計數操作。

以下是 C++ 和 Python 中自旋鎖 (Spinlock) 的範例實現。自旋鎖是一種鎖,當鎖不可用時,執行緒會在一個循環中忙等待,直到鎖可用為止。

C++ Spinlock 範例

#include <atomic>
#include <thread>
#include <iostream>
#include <vector>

class Spinlock {
public:
    Spinlock() : flag(ATOMIC_FLAG_INIT) {}

    void lock() {
        while (flag.test_and_set(std::memory_order_acquire)) {
            // 可能可以加個小延遲,讓 CPU 不至於佔用過多資源
            std::this_thread::yield(); // 提高效率,讓其他執行緒有機會執行
        }
    }

    void unlock() {
        flag.clear(std::memory_order_release);
    }

private:
    std::atomic_flag flag;
};

// 測試自旋鎖
void test_spinlock(Spinlock &spinlock, int &counter) {
    for (int i = 0; i < 100000; ++i) {
        spinlock.lock();
        ++counter; // 需要保護的共享資源
        spinlock.unlock();
    }
}

int main() {
    Spinlock spinlock;
    int counter = 0;
    std::vector<std::thread> threads;

    // 創建多個執行緒來測試自旋鎖
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back(test_spinlock, std::ref(spinlock), std::ref(counter));
    }

    // 等待所有執行緒完成
    for (auto &t : threads) {
        t.join();
    }

    std::cout << "Final counter value: " << counter << std::endl;
    return 0;
}

Python Spinlock 範例

import threading
import time

class Spinlock:
    def __init__(self):
        self.locked = False

    def lock(self):
        while True:
            # 嘗試獲得鎖
            if not self.locked:
                self.locked = True
                return
            # 短暫休眠以減少 CPU 使用率
            time.sleep(0)

    def unlock(self):
        self.locked = False

# 測試自旋鎖
def test_spinlock(spinlock, counter):
    for _ in range(100000):
        spinlock.lock()
        counter[0] += 1  # 需要保護的共享資源
        spinlock.unlock()

if __name__ == "__main__":
    spinlock = Spinlock()
    counter = [0]  # 使用列表來保證共享可變性
    threads = []

    # 創建多個執行緒來測試自旋鎖
    for _ in range(4):
        t = threading.Thread(target=test_spinlock, args=(spinlock, counter))
        threads.append(t)
        t.start()

    # 等待所有執行緒完成
    for t in threads:
        t.join()

    print(f"Final counter value: {counter[0]}")

總結

  • C++ 版本使用 std::atomic_flag 來實現自旋鎖,並在鎖不可用時使用 std::this_thread::yield() 來提高效率。
  • Python 版本使用簡單的布林變數來表示鎖的狀態,並在鎖不可用時使用 time.sleep(0) 來減少 CPU 使用率。

這兩個範例展示了如何在 C++ 和 Python 中實現自旋鎖,並在多執行緒環境中使用它們來保護共享資源。


要故意增加 context switch 數量,可以利用大量的多執行緒或多進程操作,這樣可以強制系統在不同執行緒或進程間頻繁地切換,從而增加 context switch 次數。

以下是一個 Python 程式範例,它通過多執行緒不停地進行計算操作,來增加 context switch 數量。你可以在執行這段程式碼的同時使用 perf 來觀察 context switch 數量的增長。

Python 程式碼:產生大量的 Context Switch

這個範例會啟動多個執行緒,每個執行緒進行計算並在短時間內進入睡眠,以迫使系統頻繁地在不同執行緒之間切換。

import threading
import time

def cpu_intensive_task():
    while True:
        # 模擬 CPU 密集型工作
        sum(i * i for i in range(1000))
        # 短暫休眠,讓系統有機會進行 context switch
        time.sleep(0.001)

# 啟動多個執行緒
threads = []
for _ in range(100):  # 可以調整執行緒數量來增加負載
    t = threading.Thread(target=cpu_intensive_task)
    t.start()
    threads.append(t)

# 保持主程式運行一段時間
try:
    time.sleep(10)  # 可以調整時間長度
except KeyboardInterrupt:
    pass
finally:
    # 停止所有執行緒
    for t in threads:
        t.join(timeout=0)

步驟

  1. 執行上述 Python 程式碼來創建多個執行緒。

    python3 your_script_name.py
    
  2. 同時使用 perf 工具來測量 context switch 數量。在新終端中執行:

    sudo perf stat -e context-switches sleep 10
    

    或直接執行 perf,讓它在多執行緒程式運行時計算 context switch 次數:

    sudo perf stat -e context-switches python3 your_script_name.py
    

解釋

  • 這個程式會啟動 100 個執行緒,每個執行緒都執行 CPU 密集的計算操作並短暫休眠。這種設計可以強制系統頻繁地在不同執行緒之間進行切換,從而產生大量的 context switch。
  • time.sleep(0.001) 是為了給系統一個切換執行緒的機會,加快 context switch 的頻率。
  • 調整執行緒數量(100 可以增加或減少),以及程式運行的時間(例如 sleep 10),可以控制 context switch 數量。

注意

這種方法會給 CPU 帶來較大的負擔,請在空閒時間或非生產環境下執行,以免影響系統其他進程。


在高頻交易中,記憶體破頁(page faults)對效能的影響非常顯著。高頻交易系統通常需要在微秒或更短的時間內處理大量數據,因此頻繁的破頁將會增加內存訪問延遲,降低交易速度,甚至導致潛在的延遲和損失。

以下是破頁對高頻交易的影響以及減少破頁的建議:

破頁在高頻交易中的影響

  1. 延遲增加:高頻交易的效能依賴於最低的延遲。破頁會導致 CPU 將資料從主記憶體(RAM)載入到快取,這會增加內存訪問延遲,影響下單和訂單匹配的速度。
  2. 資源浪費:頻繁的破頁會消耗額外的 CPU 時間和內存頻寬,佔用交易系統的資源,導致在高負載狀況下系統效能下降。
  3. 不可預測的延遲:破頁導致的延遲在每次發生時長度不一,這對高頻交易系統的穩定性和預測性是一個挑戰。

減少破頁的建議

  1. 記憶體對齊與連續分配:盡量使用連續的記憶體分配,如使用 numpy 或結構化的資料陣列來儲存行情或交易資料。這樣可以減少訪問時的破頁次數,提升內存的快取命中率。

  2. 使用 Huge Pages:在 Linux 上配置「大頁面(Huge Pages)」功能,讓應用程式將資料分配到大頁面,減少頁面數量,從而減少破頁次數。

  3. 減少動態分配:避免頻繁的動態記憶體分配。高頻交易系統通常會預先分配好固定大小的記憶體空間,並重複利用,減少在交易過程中動態分配和釋放記憶體的次數。

  4. 資料結構的選擇:使用內存效率高的資料結構(如陣列或 numpy)代替 Python 清單或字典,減少破頁的可能性。陣列和 numpy 陣列會在連續的內存中分配,對於高頻訪問的資料(如行情資料和訂單數據)特別有利。

  5. 關注 L1/L2 快取利用率:設計程式時,可以將頻繁使用的資料儲存在 L1/L2 快取大小內,避免頻繁訪問主記憶體。儲存高頻訪問的資料到固定大小的資料結構中,並優化內存訪問模式,最大化快取命中率。

  6. 定期的記憶體清理:對於不可避免的動態記憶體分配場景,確保定期釋放已不再需要的記憶體空間,避免破頁增多。

具體實踐

可以使用如下工具檢查內存訪問情況和破頁次數:

  • perf:可以用 perf stat -e page-faults 來監控破頁次數。
  • Huge Pages 配置
    • 確保 Linux 上的 Huge Pages 設定(如 echo 1000 > /proc/sys/vm/nr_hugepages 設置大頁面數量)。
    • 程式內啟用 mmap 等接口,專門為大頁面記憶體進行分配。

總結

減少破頁對高頻交易的效能有顯著提升。有效利用記憶體分配和管理策略,可以顯著降低記憶體延遲對交易系統的影響,從而保持交易速度的穩定性和一致性。

要讓程式引發大量的 page faults,可以使用一些會不斷分配和釋放大量記憶體的小資料結構,並頻繁訪問隨機位置的資料。相比之下,減少 page faults 的程式可以採用連續的記憶體分配方式,並在一開始就將所有資料載入。

以下是兩個範例:

  1. 第一個範例會產生大量 page faults,因為它隨機分配和訪問資料。
  2. 第二個範例則會減少 page faults,因為它使用了連續分配並優化訪問模式。

可以使用 perf 來執行這些程式,並比較 page faults 的數量。

1. 高 page faults 範例

此範例不斷隨機分配並訪問記憶體中的資料,導致頻繁的 page faults:

import random
import time

def high_page_faults():
    data = []
    try:
        for _ in range(100000):
            # 每次隨機生成大約 1 MB 的字串並放入列表中
            data.append("A" * 1024 * 1024)
            # 隨機訪問資料,增加 page faults 的機會
            _ = data[random.randint(0, len(data) - 1)]
    except MemoryError:
        print("記憶體不足,退出程式。")

if __name__ == "__main__":
    high_page_faults()

2. 低 page faults 範例

此範例會一次性分配一大塊連續的記憶體,並有序訪問資料以減少 page faults 的發生:

import numpy as np

def low_page_faults():
    # 一次性分配約 100 MB 的連續記憶體
    data = np.zeros((100, 1024 * 1024 // 8), dtype=np.float64)
    for i in range(len(data)):
        # 有序訪問記憶體中的資料
        data[i] = i

if __name__ == "__main__":
    low_page_faults()

使用 perf 比較 page faults

  1. 先執行 高 page faults 程式:

    sudo perf stat -e page-faults python3 high_page_faults.py
    
  2. 接著執行 低 page faults 程式:

    sudo perf stat -e page-faults python3 low_page_faults.py
    

結果比較

perf 執行後會顯示 page-faults 的計數結果。理論上,high_page_faults.py 應該顯示更高的 page faults 計數,而 low_page_faults.py 則會顯示較少的 page faults。這主要是因為 high_page_faults.py 使用隨機分配和訪問,導致更多的記憶體頁面被頻繁釋放和重載入;而 low_page_faults.py 使用連續記憶體和有序訪問,使快取更有效。


在高頻交易系統中,對於需要頻繁執行的任務,鎖的選擇至關重要。高頻交易要求最低延遲和高併發,因此傳統的鎖(如普通的互斥鎖或全局鎖)可能會導致性能瓶頸。以下是一些適合高頻交易系統的鎖,並介紹它們的適用情況:

1. 自旋鎖(Spinlock)

自旋鎖是一種高效的鎖,適合短時間內可以取得資源的情況。自旋鎖的運作方式是持鎖者會不斷輪詢,直到鎖被釋放。這可以避免切換上下文的開銷。

優點

  • 適合在鎖定時間非常短的情況。
  • 避免了上下文切換的開銷。

缺點

  • 如果鎖定時間長,會導致 CPU 資源浪費。

適用情況:適合非常短的臨界區,適合多執行緒的併發訪問(例如頻繁更新行情資訊)。

Python 示例

import threading

lock = threading.Lock()

def high_freq_task():
    while True:
        if lock.acquire(False):  # 嘗試自旋
            try:
                # 執行短期的臨界區代碼
                pass
            finally:
                lock.release()

2. 無鎖資料結構(Lock-Free Data Structures)

無鎖資料結構基於原子操作(如 CAS,Compare-And-Swap),在多執行緒併發訪問時無需使用鎖。這些資料結構包括無鎖佇列、無鎖堆疊等。

優點

  • 無需鎖定,避免了鎖的競爭和上下文切換。
  • 提高了執行緒間的併發度。

缺點

  • 實現複雜,且需要仔細考慮記憶體一致性問題。

適用情況:適合高度併發的任務,例如事件隊列、交易指令池等。

3. 讀寫鎖(Read-Write Lock)

如果某些資源的讀取遠多於寫入,可以使用讀寫鎖。這種鎖允許多個讀取執行緒同時讀取,但只允許一個寫入執行緒操作。

優點

  • 增加了讀取併發度,適合多讀少寫的情況。

缺點

  • 在寫入密集的情況下效能不佳。

適用情況:適合行情訂閱、讀取市場數據等多讀少寫的場景。

Python 示例(使用 threading 模組中的 RLock 作為讀寫鎖):

import threading

lock = threading.RLock()

def read_data():
    with lock:
        # 讀取操作
        pass

def write_data():
    with lock:
        # 寫入操作
        pass

4. 異步編程與協程

在一些高頻交易場景中,尤其是 I/O 密集型操作,可以避免使用鎖,改為使用協程和事件驅動的編程模式(如 Python 中的 asyncio)。這樣可以減少同步鎖的開銷,充分利用 CPU 資源。

優點

  • 無需鎖,減少了鎖競爭開銷。
  • 更適合 I/O 密集型和多網路請求的場景。

適用情況:適合 I/O 密集的操作,如多訂閱數據源的數據流處理。

Python 示例

import asyncio

async def fetch_data():
    # 異步執行讀取操作
    await asyncio.sleep(1)

async def main():
    await asyncio.gather(fetch_data(), fetch_data())

# 執行異步任務
asyncio.run(main())

總結

  • 自旋鎖:適合短期的臨界區。
  • 無鎖資料結構:適合需要高併發的數據結構(如佇列)。
  • 讀寫鎖:適合多讀少寫的場景。
  • 異步編程和協程:適合 I/O 密集型任務。

在高頻交易系統中,可以根據特定情境選擇合適的鎖或替代方法,以確保系統在高併發下仍具備低延遲和高效能。


要比較使用 Spinlock普通互斥鎖(Mutex) 的效能,可以創建一個多執行緒程式,並讓每個執行緒執行短期的臨界區操作。這樣可以觀察在高頻進入和退出臨界區的情況下,兩種鎖的性能差異。Python 中可以模擬 Spinlock 的行為,但需要注意,由於 GIL(Global Interpreter Lock)的存在,純 Python 程式的執行緒並不完全適合多核併發性能測試。以下範例展示了使用自旋行為來模擬 Spinlock,以及傳統的 Lock 比較。

範例設置

以下程式使用了兩種鎖:

  1. 自旋鎖(Spinlock):使用輪詢等待的方式模擬自旋鎖。
  2. 普通互斥鎖(Mutex):使用 Python 中的 threading.Lock 作為傳統鎖。

程式碼

import threading
import time

class Spinlock:
    def __init__(self):
        self.lock = threading.Lock()

    def acquire(self):
        # 自旋行為,直到取得鎖
        while not self.lock.acquire(blocking=False):
            pass

    def release(self):
        self.lock.release()

# 共享變數和次數
counter = 0
num_iterations = 1000000  # 每個執行緒的迭代次數

def task_with_spinlock(lock):
    global counter
    for _ in range(num_iterations):
        lock.acquire()
        counter += 1  # 進行簡單操作
        lock.release()

def task_with_mutex(lock):
    global counter
    for _ in range(num_iterations):
        lock.acquire()
        counter += 1
        lock.release()

def measure_performance(lock_type):
    global counter
    counter = 0  # 重置計數器
    num_threads = 4
    threads = []

    start_time = time.time()

    # 建立執行緒
    if lock_type == "spinlock":
        lock = Spinlock()
        for _ in range(num_threads):
            thread = threading.Thread(target=task_with_spinlock, args=(lock,))
            threads.append(thread)
    elif lock_type == "mutex":
        lock = threading.Lock()
        for _ in range(num_threads):
            thread = threading.Thread(target=task_with_mutex, args=(lock,))
            threads.append(thread)

    # 啟動所有執行緒
    for thread in threads:
        thread.start()

    # 等待所有執行緒結束
    for thread in threads:
        thread.join()

    end_time = time.time()
    print(f"{lock_type.capitalize()} duration: {end_time - start_time:.4f} seconds, Counter: {counter}")

# 測試自旋鎖和互斥鎖效能
measure_performance("spinlock")
measure_performance("mutex")

程式說明

  1. Spinlock 類別:使用自旋行為來模擬,當鎖不可用時,執行緒會不斷嘗試取得鎖而不阻塞。
  2. task_with_spinlock 與 task_with_mutex 函數:這兩個函數分別執行 SpinlockMutex 鎖的操作,對共享變數 counter 增加計數。
  3. measure_performance 函數:測試兩種鎖的效能,記錄每次測試的執行時間。

執行結果

執行該程式後,將得到自旋鎖和互斥鎖的執行時間。結果大致會如下:

Spinlock duration: 1.2345 seconds, Counter: 4000000
Mutex duration: 1.5678 seconds, Counter: 4000000

分析結果

在高頻交易環境中,Spinlock 在鎖的持有時間短時表現較佳,但如果執行緒的數量增加並且鎖持有時間增加,會導致較高的 CPU 使用率,反而降低效能。相對地,Mutex 更適合在需要稍長時間鎖定的情況,因為它會阻塞執行緒而不是不斷輪詢。


在多工或多線程的系統中,context switch(上下文切換)通常會在以下情況出現:

  1. 時間片結束:在使用時間片輪轉(time-slice round-robin)調度時,每個線程在 CPU 上執行一段時間(時間片)後,操作系統會將它切換出去,讓其他線程執行。

  2. 高優先級任務出現:當有更高優先級的任務進入等待隊列,操作系統會進行優先級調度,將當前執行的低優先級任務暫停並切換至高優先級的任務。

  3. I/O 等待:當某個線程需要等待 I/O 操作(如讀寫文件或網路請求),它會被掛起(suspended),CPU 會切換到其他線程以充分利用資源。

  4. 中斷發生:當硬體中斷(如計時器或設備中斷)發生時,操作系統可能會進行 context switch 來處理中斷,或根據情況重新分配 CPU 給其他任務。

  5. 多處理器或多核調度:在多處理器或多核心系統中,操作系統會將線程分配到不同的 CPU 核心,根據資源需求來進行上下文切換以優化資源利用。

每次 context switch 都會耗費 CPU 時間和資源,因此高效的調度策略可以減少不必要的上下文切換,提升整體系統性能。


當設計和實施高效能項目時,以下是一些關鍵的策略和最佳實踐,可以幫助提升系統性能和響應速度,特別在高頻交易和其他要求低延遲的應用中:

高效能項目總結

1. 硬體優化

  • 選擇高效能硬體:使用快速的 CPU、充足的 RAM 和高效的 SSD,特別是對於 I/O 密集型應用。
  • 網路延遲最小化:選擇低延遲的網路設備和連接,並考慮使用專用線路。

2. 記憶體管理

  • 減少記憶體破頁:增加物理記憶體,優化虛擬記憶體設定,並使用適當的資料結構。
  • 監控記憶體使用:定期檢查記憶體泄漏,並在適當時釋放不再使用的記憶體。

3. 線程和進程管理

  • 線程親和性:將特定線程綁定到特定 CPU 核心,以減少上下文切換和提高快取命中率。
  • 使用線程池:減少線程創建和銷毀的開銷,並合理安排線程數量以匹配 CPU 核心數量。

4. 減少上下文切換

  • 優化線程數量:確保線程數量與 CPU 核心數量相匹配,避免過多的線程導致頻繁切換。
  • 使用無鎖資料結構:盡量使用無鎖編程來減少鎖的競爭,降低上下文切換的開銷。

5. I/O 優化

  • 異步 I/O 操作:使用異步 I/O 來避免線程阻塞,提升系統的響應性。
  • 批量處理:合併多個小的 I/O 操作,減少系統對 I/O 的頻繁請求。

6. 程式設計優化

  • 合併任務:將多個小任務合併為大任務,減少任務切換的頻率。
  • 使用事件驅動架構:透過事件驅動設計來減少線程數量,從而降低上下文切換的頻率。

7. 性能監控與分析

  • 使用監控工具:定期使用性能監控工具(如 perf, top, htop)來追蹤系統性能,識別瓶頸。
  • 性能基準測試:對系統進行基準測試,評估其在不同負載下的性能,並根據結果進行優化。

8. 軟體架構考量

  • 分散式系統設計:考慮使用分散式架構來提升系統的可擴展性和容錯性。
  • 服務微型化:將應用拆分為小的微服務,根據需要獨立擴展,提升整體性能。

總結

在高效能項目中,關鍵在於硬體的選擇、記憶體管理、線程和進程管理、I/O 優化,以及程式設計的合理架構。通過綜合這些策略,可以大幅提升系統的性能和響應速度,特別是在對延遲要求高的應用場景中。如果你有具體的項目或情境需要進一步探討,隨時告訴我!

CPU 多核心壓力測試程式

#define _GNU_SOURCE         // 必須在所有 include 之前定義
#include <stdio.h>
#include <pthread.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>

// 定義線程參數結構體
typedef struct {
    int cpu_id;
    volatile int should_continue;  // 控制線程運行的標誌
} ThreadArg;

void* busy_work(void* arg) {
    ThreadArg* thread_arg = (ThreadArg*)arg;
    
    // 設置 CPU 親和力
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(thread_arg->cpu_id, &cpuset);
    
    pthread_t thread = pthread_self();
    if (pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset) != 0) {
        perror("pthread_setaffinity_np");
        return NULL;
    }
    
    printf("Thread started on CPU %d\n", thread_arg->cpu_id);
    
    // 模擬 CPU 密集型任務
    double result = 0.0;
    while (thread_arg->should_continue) {
        for (int i = 0; i < 1000000; i++) {
            result += i * 0.000001;
        }
    }
    
    printf("Thread on CPU %d finishing\n", thread_arg->cpu_id);
    return NULL;
}

int main() {
    const int NUM_THREADS = 4;
    pthread_t threads[NUM_THREADS];
    ThreadArg* thread_args = malloc(NUM_THREADS * sizeof(ThreadArg));
    
    if (thread_args == NULL) {
        perror("Failed to allocate memory");
        return 1;
    }
    
    // 創建線程
    for (int i = 0; i < NUM_THREADS; i++) {
        thread_args[i].cpu_id = i;
        thread_args[i].should_continue = 1;
        
        if (pthread_create(&threads[i], NULL, busy_work, &thread_args[i]) != 0) {
            perror("Failed to create thread");
            free(thread_args);
            return 1;
        }
    }
    
    // 運行一段時間後停止
    printf("Running stress test for 10 seconds...\n");
    sleep(10);
    
    // 通知所有線程停止
    for (int i = 0; i < NUM_THREADS; i++) {
        thread_args[i].should_continue = 0;
    }
    
    // 等待所有線程結束
    for (int i = 0; i < NUM_THREADS; i++) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("Failed to join thread");
        }
    }
    
    // 清理資源
    free(thread_args);
    printf("Stress test completed\n");
    
    return 0;
}

virtual function 和 template 的主要差異和使用時機:

  1. Virtual Function 的特點:
  • 運行時多態(Runtime Polymorphism)
  • 有額外的效能開銷(virtual table lookup)
  • 適合處理已知的類型層次結構
  • 主要用於繼承關係中的動態分派
  1. Template 的特點:
  • 編譯時多態(Compile-time Polymorphism)
  • 沒有運行時開銷
  • 可以處理任意類型
  • 產生專門的程式碼,可能造成程式碼膨脹

使用建議:

  1. 使用 Virtual Function 的情況:
// 當你需要在運行時根據對象類型來決定行為時
class Shape {
public:
    virtual double area() = 0;
};

class Circle : public Shape {
public:
    double area() override { return PI * radius * radius; }
private:
    double radius;
};

class Rectangle : public Shape {
public:
    double area() override { return width * height; }
private:
    double width, height;
};
  1. 使用 Template 的情況:
// 當你需要對不同類型執行相同的操作時
template<typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

// 或是泛型容器
template<typename T>
class Array {
    T* data;
    size_t size;
public:
    T& operator[](size_t index) { return data[index]; }
};
  1. 選擇準則:
  • 如果需要在運行時動態決定呼叫哪個函數 → Virtual Function
  • 如果需要處理不同但在編譯時已知的類型 → Template
  • 如果效能是關鍵考量 → Template
  • 如果需要維護類型層次結構 → Virtual Function
  1. 混合使用的情況:
// 有時候可以結合兩者的優點
template<typename T>
class Container {
public:
    virtual void process(const T& item) = 0;
};

template<typename T>
class SpecialContainer : public Container<T> {
public:
    void process(const T& item) override {
        // 特定實現
    }
};

需要注意的是,template 會在編譯期間產生多份程式碼,可能導致執行檔變大。而 virtual function 則會有一些執行時的效能開銷。選擇時要根據實際需求來權衡。

想了解更多細節或有特定的使用場景需要討論嗎?

在主流的程式語言中,C++ 算是比較晦澀和難以上手的,特別是對於新手 C++ 程式設計師,基本就是 C 語言帶 STL. 而那些 C++ 的擁躉們最引以為豪的就是 C++ 的高性能(低延遲),這樣的論調隨處可見,而關於具體如何操作卻很少提及。筆者試圖總結下這方面的實戰經驗,大家一起交流學習。部分內容來源於網路,部分來源於個人工作中的實踐。

1、高性能程式設計關注點

1. 系統層面

  • 簡化控制流程和資料流程
  • 減少消息傳遞次數
  • 負載平衡,比如避免個別伺服器成為性能瓶頸
  • 充分利用硬體性能,比如打滿 CPU
  • 減少系統額外開銷,比如上下文切換等
  • 批處理與資料預取、記憶體屏障、綁核、偽共享、核隔離等

2. 演算法層面

  • 高效演算法降低時間和空間複雜度
  • 高效的資料結構設計,比如

C++ 資料結構設計:如何高效地儲存並操作超大規模的 76 贊同 · 7 評論文章

  • 增加任務的並行性(如協程)、減少鎖的開銷(lock_free)

3. 程式碼層面

  • I-cache(指令),D-cache(資料) 最佳化
  • 程式碼執行順序的調整,比如減少分支預測失敗率
  • 編譯最佳化選項,比如 PGO、LTO、BOLT等
  • 語言本身相關的最佳化技巧
  • 減少函數呼叫棧的深度
  • 操作放置到編譯期執行,比如範本
  • 延遲計算:(1)兩端建構(當實例能夠被靜態地建構時,經常會缺少建構對象所需的資訊。在建構對象時,我們並 不是一氣呵成,而是僅在建構函式中編寫建立空對象的最低限度的程式碼。稍後,程序再 呼叫該對象的初始化成員函數來完成建構。將初始化推遲至有足夠的額外資料時,意味 著被建構的對象總是高效的、扁平的資料結構;(2)寫時複製(指當一個對象被覆制時,並不複製它的動態成員變數,而是讓兩個實例共享動態變數。只在其中某個實例要修改該變數時,才會真正進行複製)

2、預置知識 - Cache

1. Cache hierarchy

Cache(快取)一般分為 3 級:L1、L2、L3. 通常來說 L1、L2是整合在 CPU 裡面的(可以稱之為On-chip cache),而 L3 是放在 CPU 外面(可以稱之為 Off-chip cache)。當然這個不是絕對的,不同 CPU 的做法可能會不太一樣。當然,Register(暫存器)裡的資料讀寫是最快的。比如,矩陣乘法最佳化:

寨森Lambda-CDM:C++加速矩陣乘法的最簡單方法508 贊同 · 40 評論文章img

2. Cache size

Cache 的容量決定了有多少程式碼和資料可以放到 Cache 裡面,如果一個程序的熱點(hotspot)已經完全填充了整個 Cache,那 麼再從 Cache 角度考慮最佳化就是白費力氣了。

3. Cache line size

CPU 從記憶體 Load 資料是一次一個 cache line;往記憶體裡面寫也是一次一個 cache line,所以一個 cache line 裡面的資料最好是讀寫分開,否則就會相互影響。

4. Cache associative

全關聯(full associative):記憶體可以對應到任意一個 Cache line;

N-way 關聯:這個就是一個雜湊表的結構,N 就是衝突鏈的長度,超過了 N,就需要替換。

5. Cache type

I-cache(指令)、D-cache(資料)、TLB(MMU 的 cache),參考:

https://en.wikipedia.org/wiki/CPU_cacheen.wikipedia.org/wiki/CPU_cache

3、系統最佳化方法

1. Asynchronous

非同步,yyds!

2. Polling

Polling 是網路裝置裡面常用的一個技術,比如 Linux 的 NAPI 或者 epoll。與之對應的是中斷,或者是事件。Polling 避免了狀態切換的開銷,所以有更高的性能。但是,如果系統裡面有多種任務,如何在 Polling 的時候,保證其他任務的執行時間?Polling 通常意味著獨佔,此時系統無法響應其他事件,可能會造成嚴重後果。凡是能用事件或中斷的地方都能用 Polling 替代,是否合理,需要結合系統的資料流程來決定。

3. 靜態記憶體池

靜態記憶體有更好的性能,但是適應性較差(特別是系統裡面有多個 任務的時候),而且會有浪費(提前分配,還沒用到就分配了)。

4. 並行最佳化:lock-free 和 lock-less。

lock-free 是完全無鎖的設計,有兩種實現方式:

• Per-cpu data, 上文已經提及過,就是 thread local

• CAS based,CAS 是 compare and swap,這是一個原子操作(spinlock 的實現同樣需要 compare and swap,但區別是 spinlock 只有兩個狀態 LOCKED 和 UNLOCKED,而 CAS 的變數可以有多個狀態);其次,CAS 的實現必須由硬體來保障(原子操作),CAS 一次可以操作 32 bits,也有 MCAS,一次可以修改一塊記憶體。基於 CAS 實現的資料結構沒有一個統一、一致的實現方法,所以有時不如直接加鎖的演算法那麼簡單,直接,針對不同的資料結構,有不同的 CAS 實現方法,讀者可以自己搜尋。

lock-less 的目的是減少鎖的爭用(contention),而不是減少鎖。這個和鎖的粒度(granularity)相關,鎖的粒度越小,等待的時間就越短,並行的時間就越長。

鎖的爭用,需要考慮不同執行緒在獲取鎖後,會執行哪些不同的動作。比如多執行緒佇列,一般情況下,我們一把鎖鎖住整個佇列,性能很差。如果所有的 enqueue 操作都是往佇列的尾部插入新節點,而所有的 dequeue 操作都是從佇列的頭部刪除節點,那麼 enqueue 和 dequeue 大部分時候都是相互獨立的,我們大部分時候根本不需要鎖住整個佇列,白白損失性能!那麼一個很自然就能想到的演算法最佳化方案就呼之慾出了:我們可以把那個佇列鎖拆成兩個:一個佇列頭部鎖(head lock)和一個佇列尾部鎖(tail lock),偽程式碼如下:

typedef struct node_t {
    TYPE value; 
    node_t *next
} NODE;
 
typedef struct queue_t {
    NODE *head; 
    NODE *tail;
    LOCK q_h_lock;
    LOCK q_t_lock;
} Q;
 
initialize(Q *q) {
   node = new_node()   // Allocate a free node
   node->next = NULL   // Make it the only node in the linked list
   q->head = q->tail = node   // Both head and tail point to it
   q->q_h_lock = q->q_t_lock = FREE   // Locks are initially free
}
 
enqueue(Q *q, TYPE value) {
   node = new_node()       // Allocate a new node from the free list
   node->value = value     // Copy enqueued value into node
   node->next = NULL       // Set next pointer of node to NULL
   lock(&q->q_t_lock)      // Acquire t_lock in order to access Tail
      q->tail->next = node // Link node at the end of the queue
      q->tail = node       // Swing Tail to node
   unlock(&q->q_t_lock)    // Release t_lock
}
 
dequeue(Q *q, TYPE *pvalue) {
   lock(&q->q_h_lock)   // Acquire h_lock in order to access Head
      node = q->head    // Read Head
      new_head = node->next       // Read next pointer
      if new_head == NULL         // Is queue empty?
         unlock(&q->q_h_lock)     // Release h_lock before return
         return FALSE             // Queue was empty
      endif
      *pvalue = new_head->value   // Queue not empty, read value
      q->head = new_head  // Swing Head to next node
   unlock(&q->q_h_lock)   // Release h_lock
   free(node)             // Free node
   return TRUE            // Queue was not empty, dequeue succeeded
}

具體實現可參考:高性能多執行緒佇列

Fast Concurrent Queue Algorithmswww.cs.rochester.edu/research/synchronization/pseudocode/queues.htmlwww.cs.rochester.edu/research/synchronization/pseudocode/queues.html

5. 處理程序間通訊 - 共用記憶體

關於各種處理程序間通訊的方式詳細介紹和比較,下面這篇文章講得非常詳細:

涼了!張三同學沒答好「處理程序間通訊」,被面試官掛了....mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247485318&idx=1&sn=0da0a684639106f548e9d4454fd49904&scene=21#wechat_redirectmp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247485318&idx=1&sn=0da0a684639106f548e9d4454fd49904&scene=21#wechat_redirect

對於本地處理程序間需要高頻次的大量資料互動,首推共用記憶體這種方案。

現代作業系統普遍採用了基於虛擬記憶體的管理方案,在這種記憶體管理方式之下,各個處理程序之間進行了強制隔離。程式碼中使用的記憶體地址均是一個虛擬地址,由作業系統的記憶體管理演算法提前分配對應到對應的實體記憶體頁面,CPU在執行程式碼指令時,對訪問到的記憶體地址再進行即時的轉換翻譯。

img

從上圖可以看出,不同處理程序之中,雖然是同一個記憶體地址,最終在作業系統和 CPU 的配合下,實際儲存資料的記憶體頁面卻是不同的。而共用記憶體這種處理程序間通訊方案的核心在於:如果讓同一個實體記憶體頁面對應到兩個處理程序地址空間中,雙方不是就可以直接讀寫,而無需複製了嗎?

img

當然,共用記憶體只是最終的資料傳輸載體,雙方要實現通訊還得藉助訊號、訊號量等其他通知機制。

6. I/O 最佳化 - 多路復用技術

網路程式設計中,當每個執行緒都要阻塞在 recv 等待對方的請求,如果訪問的人多了,執行緒開的就多了,大量執行緒都在阻塞,系統運轉速度也隨之下降。這個時候,你需要多路復用技術,使用 select 模型,將所有等待(accept、recv)都放在主執行緒裡,工作執行緒不需要再等待。

img

但是,select 不能應付海量的網站存取。這個時候,你需要升級多路復用模型為 epoll。select 有三弊,epoll 有三優:

  • select 底層採用陣列來管理套接字描述符,同時管理的數量有上限,一般不超過幾千個,epoll使用樹和鏈表來管理,同時管理數量可以很大
  • select不會告訴你到底哪個套接字來了消息,你需要一個個去詢問。epoll 直接告訴你誰來了消息,不用輪詢
  • select進行系統呼叫時還需要把套接字列表在使用者空間和核心空間來回複製,循環中呼叫 select 時簡直浪費。epoll 統一在核心管理套接字描述符,無需來回複製

7. 執行緒池技術

使用一個公共的任務佇列,請求來臨時,向佇列中投遞任務,各個工作執行緒統一從佇列中不斷取出任務來處理,這就是執行緒池技術。

img

多執行緒技術的使用一定程度提升了伺服器的並行能力,但同時,多個執行緒之間為了資料同步,常常需要使用互斥體、訊號、條件變數等手段來同步多個執行緒。這些重量級的同步手段往往會導致執行緒在使用者態/核心態多次切換,系統呼叫,執行緒切換都是不小的開銷。具體實現,請參考這篇文章:

C++ 多執行緒(四):實現一個功能完整的執行緒池12 贊同 · 4 評論文章

4、演算法最佳化

比如高效的過濾演算法、雜湊演算法、分治演算法等等,大家在刷題的過程中估計都能感受到演算法的魅力了,這裡不再贅述。

5、程式碼層次最佳化

1. I-cache 最佳化

一是相關的原始檔要放在一起;二是相關的函數在object檔案裡面,也應該是相鄰的。這樣,在可執行檔案被載入到記憶體裡面的時候,函數的位置也是相鄰的。相鄰的函數,衝突的機率比較小。而且相關的函數放在一起,也符合模組化程式設計的要求:那就是 高內聚,低耦合。

如果能夠把一個 code path 上的函數編譯到一起(需要編譯器支援,把相關函數編譯到一起), 很顯然會提高 I-cache 的命中率,減少衝突。但是一個系統有很多個 code path,所以不可能面面俱到。不同的性能指標,在最佳化的時候可能是衝突的。所以儘量做對所以 case 都有效的最佳化,雖然做到這一點比較難。

常見的手段有函數重排(獲取程式執行軌跡,重排二進制目標檔案(elf 檔案)裡的程式碼段)、函數冷熱分區等。

https://github.com/facebookincubator/BOLTgithub.com/facebookincubator/BOLT

2. D-cache相關最佳化

  • Cache line alignment (cache 對齊)

資料跨越兩個 cacheline,就意味著兩次 load 或者兩次 store。如果資料結構是 cacheline 對齊的,就有可能減少一次讀寫。資料結構的首地址 cache line 對齊,意味著可能有記憶體浪費(特別是陣列這樣連續分配的資料結構),所以需要在空間和時間兩方面權衡。

  • 分支預測

likely/unlikely

  • Data prefetch (資料預取)

使用 X86 架構下 gcc 內建的預取指令集:

 #include <time.h>
 #include <stdio.h>
 #include <stdlib.h>

 int binarySearch(int *array, int number_of_elements, int key) {
         int low = 0, high = number_of_elements-1, mid;
         while(low <= high) {
                 mid = (low + high)/2;
            #ifdef DO_PREFETCH
            // low path
            __builtin_prefetch (&array[(mid + 1 + high)/2], 0, 1);
            // high path
            __builtin_prefetch (&array[(low + mid - 1)/2], 0, 1);
            #endif

                 if(array[mid] < key)
                         low = mid + 1; 
                 else if(array[mid] == key)
                         return mid;
                 else if(array[mid] > key)
                         high = mid-1;
         }
         return -1;
 }
  • Register parameters (暫存器參數)

一般來說,函數呼叫的參數少於某個數,比如 3,參數是通過暫存器傳遞的(這個要看 ABI 的約定)。所以,寫函數的時候,不要帶那麼多參數。

  • Lazy computation (延遲計算)

延遲計算的意思是最近用不上的變數,就不要去初始化。通常來說,在函數開始就會初始化很多資料,但是這些資料在函數執行過程中並沒有用到(比如一個分支判斷,就退出了函數),那麼這些動作就是浪費了。

變數初始化是一個好的程式設計習慣,但是在性能最佳化的時候,有可能就是一個多餘的動作,需要綜合考慮函數的各個分支,做出決定。

延遲計算也可以是系統層次的最佳化,比如 COW(copy-on-write) 就是在 fork 子處理程序的時候,並沒有複製父處理程序所有的頁表,而是隻複製指令部分。當有寫發生的時候,再複製資料部分,這樣可以避免不必要的複製,提供處理程序建立的速度。

  • Early computation (提前計算)

有些變數,需要計算一次,多次使用的時候。最好是提前計算一下,保存結果,以後再引用,避免每次都重新計算一次。

  • Allocation on stack (局部變數)

適當定義一些全域變數避免棧上的變數

  • Per-cpu data structure (非共享的資料結構)

比如並行程式設計時,給每個執行緒分配獨立的記憶體空間

  • Move exception path out (把 exception 處理放到另一個函數裡面)

只要引入了異常機制,無論系統是否會拋出異常,異常程式碼都會影響程式碼的大小與性能;未觸發異常時對系統影響並不明顯,主要影響一些編譯最佳化手段;觸發異常之後按異常實現機制的不同,其對系統性能的影響也不相同,不過一般很明顯。所以,不用擔心異常對正常程式碼邏輯性能的影響,同時不要借用異常機制處理業務邏輯。現代 C++ 編譯器所使用的異常機制對正常程式碼性能的影響並不明顯,只有出現異常的時候異常機制才會影響整個系統的性能,這裡有一些測試資料。

另外,把 exception path 和 critical path 放到一起(程式碼混合在一起),就會影響 critical path 的 cache 性能。而很多時候,exception path 都是長篇大論,有點喧賓奪主的感覺。如果能把 critical path 和 exception path 完全分離開,這樣對 i-cache 有很大幫助

  • Read, write split (讀寫分離)

偽共享(false sharing):就是說兩個無關的變數,一個讀,一個寫,而這兩個變數在一個cache line裡面。那麼寫會導致cache line失效(通常是在多核程式設計裡面,兩個變數在不同的core上引用)。讀寫分離是一個很難運用的技巧,特別是在code很複雜的情況下。需要不斷地偵錯,是個力氣活(如果有工具幫助會好一點,比如 cache miss時觸發 cpu 的 execption 處理之類的)

以C++為核心語言的高頻交易系統是如何做到低延遲的?958 贊同 · 29 評論回答

6、總結

上面所列舉的大多數還是通用的高性能程式設計手段,從物理硬體 CPU、記憶體、硬碟、網路卡到軟體層面的通訊、快取、演算法、架構每一個環節的最佳化都是通往高性能的道路。軟體性能瓶頸定位的常用手段有 perf(火焰圖)以及在 Intel CPU 上使用 pmu-tools 進行 TopDown 分析。接下來,我們將從 C++ 程式語言本身層面出發,探討下不同場景下最高效的 C++ 程式碼實現方式。

img

C++ 資料結構設計:如何高效地儲存並操作超大規模的 <KEY, VALUE>

在搜、廣、推場景中,Embedding 層有海量的稀疏參數(以 <key, value> 的形式儲存在參數伺服器上),規模可達千億等級。其中,key 的類型是 uint64_t,value 的類型是 float 類型的陣列,而且這個陣列的長度對於不同的模型是可變的。那麼,如何設計這樣一個儲存結構並能實現最高效地增、刪、改、查呢?

方案 1:

純 map 實現,log(n) 的複雜度

  • 優點:實現簡單,直接呼叫 stl 庫或者第三方 hash_map 即可
  • 缺點:大量的記憶體申請、釋放操作,而且會產生大量的記憶體碎片,開銷非常大

方案2:

標準的 hash 表,分桶(bucket),每個桶裡使用鏈表

優點:實現相對簡單

缺點:查詢的時候,定位到具體的桶 id 之後,還需要遍歷鏈表

img

方案3:

和方案2基本差不多,區別是桶裡的鏈表用 map 實現

優點:查詢的速度比方案 2 快

缺點:記憶體分配和釋放及記憶體碎片的問題還是沒得到解決

終極方案:

在方案 3 的基礎上,加上動態記憶體技術(見下圖)。簡單來說,就是每次申請固定個數(比如 64)的節點記憶體(鏈表形式),每個節點的記憶體大小是 sizeof(VALUE),分別用兩個指針表示空閒鏈表(綠色部分,表示可用)和佔用鏈表(紅色部分,已使用)

優點:增、刪、改、查速度都得到大大提升

img

實現(參見 Paddle 開放原始碼):

1. 記憶體分配器

template <class T>
class ChunkAllocator {
   public:
    explicit ChunkAllocator(size_t chunk_size = 64) {
        CHECK(sizeof(Node) == std::max(sizeof(void*), sizeof(T)));
        _chunk_size = chunk_size;
        _chunks = NULL;
        _free_nodes = NULL;
        _counter = 0;
    }
    ChunkAllocator(const ChunkAllocator&) = delete;
    ~ChunkAllocator() {
        while (_chunks != NULL) {
            Chunk* x = _chunks;
            _chunks = _chunks->next;
            free(x);
        }
    }
    template <class... ARGS>
    T* acquire(ARGS&&... args) {
        if (_free_nodes == NULL) {
            create_new_chunk();
        }

        T* x = (T*)(void*)_free_nodes;  // NOLINT
        _free_nodes = _free_nodes->next;
        new (x) T(std::forward<ARGS>(args)...);
        _counter++;
        return x;
    }
    void release(T* x) {
        x->~T();
        Node* node = (Node*)(void*)x;  // NOLINT
        node->next = _free_nodes;
        _free_nodes = node;
        _counter--;
    }
    size_t size() const { return _counter; }

   private:
    struct alignas(T) Node {
        union {
            Node* next;
            char data[sizeof(T)];
        };
    };
    struct Chunk {
        Chunk* next;
        Node nodes[];
    };

    size_t _chunk_size;  // how many elements in one chunk
    Chunk* _chunks;      // a list
    Node* _free_nodes;   // a list
    size_t _counter;     // how many elements are acquired

    void create_new_chunk() {
        Chunk* chunk;
        posix_memalign(reinterpret_cast<void**>(&chunk),
                       std::max<size_t>(sizeof(void*), alignof(Chunk)),
                       sizeof(Chunk) + sizeof(Node) * _chunk_size);
        chunk->next = _chunks;
        _chunks = chunk;

        for (size_t i = 0; i < _chunk_size; i++) {
            Node* node = &chunk->nodes[i];
            node->next = _free_nodes;
            _free_nodes = node;
        }
    }
};

2. SparseTableShard

#include <mct/hash-map.hpp>

template <class KEY, class VALUE>
struct alignas(64) SparseTableShard {
   public:
    typedef typename mct::closed_hash_map<KEY, mct::Pointer, std::hash<KEY>>
        map_type;
    struct iterator {
        typename map_type::iterator it;
        size_t bucket;
        map_type* buckets;
        friend bool operator==(const iterator& a, const iterator& b) {
            return a.it == b.it;
        }
        friend bool operator!=(const iterator& a, const iterator& b) {
            return a.it != b.it;
        }
        const KEY& key() const { return it->first; }
        VALUE& value() const { return *(VALUE*)(void*)it->second; }  // NOLINT
        VALUE* value_ptr() const {
            return (VALUE*)(void*)it->second;
        }  // NOLINT
        iterator& operator++() {
            ++it;

            while (it == buckets[bucket].end() &&
                   bucket + 1 < CTR_SPARSE_SHARD_BUCKET_NUM) {
                it = buckets[++bucket].begin();
            }

            return *this;
        }
        iterator operator++(int) {
            iterator ret = *this;
            ++*this;
            return ret;
        }
    };
    struct local_iterator {
        typename map_type::iterator it;
        friend bool operator==(const local_iterator& a,
                               const local_iterator& b) {
            return a.it == b.it;
        }
        friend bool operator!=(const local_iterator& a,
                               const local_iterator& b) {
            return a.it != b.it;
        }
        const KEY& key() const { return it->first; }
        VALUE& value() const { return *(VALUE*)(void*)it->second; }  // NOLINT
        local_iterator& operator++() {
            ++it;
            return *this;
        }
        local_iterator operator++(int) { return {it++}; }
    };

    ~SparseTableShard() { clear(); }
    bool empty() { return _alloc.size() == 0; }
    size_t size() { return _alloc.size(); }
    void set_max_load_factor(float x) {
        for (size_t bucket = 0; bucket < CTR_SPARSE_SHARD_BUCKET_NUM;
             bucket++) {
            _buckets[bucket].max_load_factor(x);
        }
    }
    size_t bucket_count() { return CTR_SPARSE_SHARD_BUCKET_NUM; }
    size_t bucket_size(size_t bucket) { return _buckets[bucket].size(); }
    void clear() {
        for (size_t bucket = 0; bucket < CTR_SPARSE_SHARD_BUCKET_NUM;
             bucket++) {
            map_type& data = _buckets[bucket];
            for (auto it = data.begin(); it != data.end(); ++it) {
                _alloc.release((VALUE*)(void*)it->second);  // NOLINT
            }
            data.clear();
        }
    }
    iterator begin() {
        auto it = _buckets[0].begin();
        size_t bucket = 0;
        while (it == _buckets[bucket].end() &&
               bucket + 1 < CTR_SPARSE_SHARD_BUCKET_NUM) {
            it = _buckets[++bucket].begin();
        }
        return {it, bucket, _buckets};
    }
    iterator end() {
        return {_buckets[CTR_SPARSE_SHARD_BUCKET_NUM - 1].end(),
                CTR_SPARSE_SHARD_BUCKET_NUM - 1, _buckets};
    }
    local_iterator begin(size_t bucket) { return {_buckets[bucket].begin()}; }
    local_iterator end(size_t bucket) { return {_buckets[bucket].end()}; }
    iterator find(const KEY& key) {
        size_t hash = _hasher(key);
        size_t bucket = compute_bucket(hash);
        auto it = _buckets[bucket].find_with_hash(key, hash);
        if (it == _buckets[bucket].end()) {
            return end();
        }
        return {it, bucket, _buckets};
    }
    VALUE& operator[](const KEY& key) { return emplace(key).first.value(); }
    std::pair<iterator, bool> insert(const KEY& key, const VALUE& val) {
        return emplace(key, val);
    }
    std::pair<iterator, bool> insert(const KEY& key, VALUE&& val) {
        return emplace(key, std::move(val));
    }
    template <class... ARGS>
    std::pair<iterator, bool> emplace(const KEY& key, ARGS&&... args) {
        size_t hash = _hasher(key);
        size_t bucket = compute_bucket(hash);
        auto res = _buckets[bucket].insert_with_hash({key, NULL}, hash);

        if (res.second) {
            res.first->second = _alloc.acquire(std::forward<ARGS>(args)...);
        }

        return {{res.first, bucket, _buckets}, res.second};
    }
    iterator erase(iterator it) {
        _alloc.release((VALUE*)(void*)it.it->second);  // NOLINT
        size_t bucket = it.bucket;
        auto it2 = _buckets[bucket].erase(it.it);
        while (it2 == _buckets[bucket].end() &&
               bucket + 1 < CTR_SPARSE_SHARD_BUCKET_NUM) {
            it2 = _buckets[++bucket].begin();
        }
        return {it2, bucket, _buckets};
    }
    void quick_erase(iterator it) {
        _alloc.release((VALUE*)(void*)it.it->second);  // NOLINT
        _buckets[it.bucket].quick_erase(it.it);
    }
    local_iterator erase(size_t bucket, local_iterator it) {
        _alloc.release((VALUE*)(void*)it.it->second);  // NOLINT
        return {_buckets[bucket].erase(it.it)};
    }
    void quick_erase(size_t bucket, local_iterator it) {
        _alloc.release((VALUE*)(void*)it.it->second);  // NOLINT
        _buckets[bucket].quick_erase(it.it);
    }
    size_t erase(const KEY& key) {
        auto it = find(key);
        if (it == end()) {
            return 0;
        }
        quick_erase(it);
        return 1;
    }
    size_t compute_bucket(size_t hash) {
        if (CTR_SPARSE_SHARD_BUCKET_NUM == 1) {
            return 0;
        } else {
            return hash >>
                   (sizeof(size_t) * 8 - CTR_SPARSE_SHARD_BUCKET_NUM_BITS);
        }
    }

   private:
    map_type _buckets[CTR_SPARSE_SHARD_BUCKET_NUM];
    ChunkAllocator<VALUE> _alloc;
    std::hash<KEY> _hasher;
};

3. 使用示例

class FixedFeatureValue {
   public:
    FixedFeatureValue() {}
    ~FixedFeatureValue() {}
    float* data() { return _data.data(); }
    size_t size() { return _data.size(); }
    void resize(size_t size) { _data.resize(size); }
    void shrink_to_fit() { _data.shrink_to_fit(); }

   private:
    std::vector<float> _data;
};

typedef SparseTableShard<uint64_t, FixedFeatureValue> shard_type;

時間轉換程式分析報告

一、程式目的

本程式主要用於解決以下問題:

  • 輸入兩個 HHMMSS 格式的時間(例如 091832 表示 09:18:32)
  • 將這些時間轉換為秒數
  • 計算兩個時間之間的差異(以秒為單位)

二、整數乘法與除法效能測試

#include <iostream>
#include <chrono>

// 整數 (int64_t) 乘除法函數
int64_t mulInt(int64_t a) {
    return static_cast<int64_t>(a * 0.01);  // 整數乘法,結果為整數
}

int64_t divInt(int64_t a) {
    return a / 100;  // 整數除法,結果為整數
}

// 無符號整數 (uint64_t) 乘除法函數
uint64_t mulUInt(uint64_t a) {
    return static_cast<uint64_t>(a * 0.01);  // 無符號整數乘法,結果為無符號整數
}

uint64_t divUInt(uint64_t a) {
    return a / 100;  // 無符號整數除法,結果為無符號整數
}

// 位移測試函數
int64_t shiftInt(int64_t a) {
    return a << 1;  // 左位移
}

int64_t shiftUInt(uint64_t a) {
    return a << 1;  // 左位移
}

// 性能測試函數
int64_t testMathCalc() {
    const auto total_iterations = 10000 * 10000;

    // 計算整數乘法運行時間
    auto start_sys_time = std::chrono::system_clock::now();
    int64_t value = 0;
    for (auto i = 0; i < total_iterations; ++i) {
        value = mulInt(i + 5 + value);
    }
    auto elapsedSysTime = std::chrono::system_clock::now() - start_sys_time;
    auto lossTime = std::chrono::duration_cast<std::chrono::microseconds>(elapsedSysTime).count();
    std::cout << "mulInt (int64) lossTime: " << lossTime << " us." << std::endl;

    // 計算整數除法運行時間
    start_sys_time = std::chrono::system_clock::now();
    for (auto i = 0; i < total_iterations; ++i) {
        value = divInt(i + 5 + value);
    }
    elapsedSysTime = std::chrono::system_clock::now() - start_sys_time;
    lossTime = std::chrono::duration_cast<std::chrono::microseconds>(elapsedSysTime).count();
    std::cout << "divInt (int64) lossTime: " << lossTime << " us." << std::endl;

    // 計算無符號整數乘法運行時間
    uint64_t uValue = 0;
    start_sys_time = std::chrono::system_clock::now();
    for (auto i = 0; i < total_iterations; ++i) {
        uValue = mulUInt(i + 5 + uValue);
    }
    elapsedSysTime = std::chrono::system_clock::now() - start_sys_time;
    lossTime = std::chrono::duration_cast<std::chrono::microseconds>(elapsedSysTime).count();
    std::cout << "mulUInt (uint64) lossTime: " << lossTime << " us." << std::endl;

    // 計算無符號整數除法運行時間
    start_sys_time = std::chrono::system_clock::now();
    for (auto i = 0; i < total_iterations; ++i) {
        uValue = divUInt(i + 5 + uValue);
    }
    elapsedSysTime = std::chrono::system_clock::now() - start_sys_time;
    lossTime = std::chrono::duration_cast<std::chrono::microseconds>(elapsedSysTime).count();
    std::cout << "divUInt (uint64) lossTime: " << lossTime << " us." << std::endl;

    // 計算整數左位移運行時間
    start_sys_time = std::chrono::system_clock::now();
    for (auto i = 0; i < total_iterations; ++i) {
        value = shiftInt(i + 5 + value);
    }
    elapsedSysTime = std::chrono::system_clock::now() - start_sys_time;
    lossTime = std::chrono::duration_cast<std::chrono::microseconds>(elapsedSysTime).count();
    std::cout << "shiftInt (int64) lossTime: " << lossTime << " us." << std::endl;

    // 計算無符號整數左位移運行時間
    start_sys_time = std::chrono::system_clock::now();
    for (auto i = 0; i < total_iterations; ++i) {
        uValue = shiftUInt(i + 5 + uValue);
    }
    elapsedSysTime = std::chrono::system_clock::now() - start_sys_time;
    lossTime = std::chrono::duration_cast<std::chrono::microseconds>(elapsedSysTime).count();
    std::cout << "shiftUInt (uint64) lossTime: " << lossTime << " us." << std::endl;

    return value + uValue;
}

int main(int argc, char* argv[]) {
    testMathCalc();
    return 0;
}

這裡整理了一個表格來比較使用 g++ -O0g++ -O3 的未優化和優化版本下的運算性能。這樣的對比能清楚顯示各種運算的時間損耗及其變化。

性能測試結果

運算類型-O0 時間損耗 (us)-O3 時間損耗 (us)
整數乘法 (mulInt)394511315831
整數除法 (divInt)145692145684
無符號整數乘法 (mulUInt)309761313527
無符號整數除法 (divUInt)127548127633
整數左位移 (shiftInt)7369436464
無符號整數左位移 (shiftUInt)7238936521

註解

  • -O0: 未進行任何優化的編譯版本。
  • -O3: 進行高級別優化的編譯版本。
  • 左位移運算: 在兩種編譯版本中,位移操作的性能均為最佳。
  • 乘法和除法性能: 在乘法和除法運算中,無符號整數的性能略低於有符號整數,但整體來說,有符號整數的表現相對較好。

這個表格清晰地展示了不同編譯優化選項下的性能變化,特別是左位移操作在兩種模式下的優勢。

二、實作方法

程式提供了三種不同的實作方法來解決此問題:

1. 基本版本 (Basic Conversion)

unsigned int convert_time_to_seconds_basic(unsigned int time_in_hhmmss)
{
    unsigned int hours = time_in_hhmmss / TIME_HOUR_DIVISOR;
    unsigned int minutes = (time_in_hhmmss / TIME_MINUTE_DIVISOR) % 100;
    unsigned int seconds = time_in_hhmmss % 100;
    return (hours * SECONDS_PER_HOUR) + (minutes * SECONDS_PER_MINUTE) + seconds;
}
  • 使用基本的數學運算
  • 透過除法和取餘數來分離時、分、秒
  • 計算方式直觀易懂
  • 需要多次除法運算,效能較差

2. 優化版本 (Optimized Conversion)

unsigned int convert_time_to_seconds_optimized(unsigned int time_in_hhmmss)
{
    unsigned int hour = ((uint64_t)time_in_hhmmss * HOUR_RECIPROCAL) >> 32;
    unsigned int remaining_time = time_in_hhmmss - hour * TIME_HOUR_DIVISOR;
    unsigned int minute = ((uint64_t)remaining_time * MINUTE_RECIPROCAL) >> 32;
    return time_in_hhmmss - (6400 * hour) - (40 * minute);
}

特點:

  • 使用位移運算代替除法
  • 採用預先計算的倒數來優化運算
  • 透過減法而非乘法來計算最終結果
  • HOUR_RECIPROCAL (429496U) 和 MINUTE_RECIPROCAL (42949672U) 是經過特殊計算的常數

3. 查表法版本 (Lookup Table)

unsigned int convert_time_to_seconds_using_table(unsigned int time_in_hhmmss)
{
    return time_seconds[time_in_hhmmss];
}

特點:

  • 使用預先計算好的查找表
  • 直接透過索引取得結果
  • 犧牲記憶體空間換取執行速度
  • 表格中使用 -1 標記無效的時間值

三、效能比較

程式使用了以下參數進行測試:

  • 測試次數:400,000,000 次
  • 測試數值:91832 (09:18:32) 和 154957 (15:49:57)

效能比較(執行時間由慢到快):

  1. 基本版本:需要多次除法運算,效能最差
  2. 優化版本:使用位移運算和特殊常數,效能提升明顯
  3. 查表法:直接查表獲得結果,效能最佳
  • 使用Clang -O0 比較三者效能排序為:查表法 > 優化版本 > 基本版本

  • 使用Clang -O3 比較三者效能排序為: 查表法 > 優化版本 > 基本版本 但沒有未優化明顯

  • GCC -O3 編譯器優化下,各版本在 400,000,000 次迭代中的執行時間,查表法 > 優化版本 > 基本版本
版本執行時間 (秒)編譯器優化等級測試迭代次數
基本版本1.030993gcc -O3400,000,000
優化版本0.587336gcc -O3400,000,000
查表法0.216822gcc -O3400,000,000

數據分析

  • 所有轉換函數均在 gcc -O3 優化級別下執行。
  • 測試的迭代次數均為 400,000,000,保證了性能比較的公平性。

這樣的整理應該可以讓您更清晰地展示執行時間的差異。如果您需要進一步的圖形化或其他資訊,隨時告訴我!

四、記憶體使用分析

各版本的記憶體使用特性:

  1. 基本版本:只需要少量的變數空間
  2. 優化版本:需要額外儲存預先計算的常數
  3. 查表法:需要大量記憶體存放查找表

五、使用建議

  1. 如果記憶體空間充足,建議使用查表法,可獲得最佳效能
  2. 如果記憶體受限但仍需要較好的效能,可使用優化版本
  3. 基本版本適合用於教學或需要程式碼易讀性的場合

六、測試工具

  • GCC:使用 GCC 編譯器進行編譯與優化測試。
  • objdump:可用於檢查編譯後的二進位檔,幫助進行性能分析和優化。
  • Quick-Bench:在線性能測試平台,便於快速比較不同程式片段的效能。
    • https://quick-bench.com/
  • Godbolt Compiler Explorer:提供多種編譯器的即時代碼檢查和性能測試,便於分析和學習不同編譯器的行為。
    • https://gcc.godbolt.org

C++ 智能指標指南:unique_ptr vs shared_ptr

概述

智能指標是 C++11 引入的重要特性,用於自動管理記憶體生命週期,避免記憶體洩漏和懸空指標問題。

主要差異對比

特性std::unique_ptrstd::shared_ptr
所有權獨佔所有權共享所有權
複製不可複製,只能移動可複製
效能幾乎無開銷有參考計數開銷
記憶體使用只存儲指標存儲指標 + 控制塊
執行緒安全不保證參考計數操作是執行緒安全的
適用場景單一擁有者多個擁有者

std::unique_ptr

特性

  • 獨佔所有權:同一時間只能有一個 unique_ptr 擁有物件
  • 移動語意:支援移動但不支援複製
  • 零開銷:沒有額外的記憶體和效能開銷
  • 自動清理:超出作用域時自動釋放記憶體

基本用法

#include <memory>

// 建立 unique_ptr
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
std::unique_ptr<int> ptr2(new int(42));  // 較不推薦

// 使用
*ptr1 = 100;
std::cout << *ptr1 << std::endl;

// 移動所有權
std::unique_ptr<int> ptr3 = std::move(ptr1);
// ptr1 現在是 nullptr

// 重置
ptr3.reset(new int(200));

// 釋放所有權並取得原始指標
int* raw_ptr = ptr3.release();
delete raw_ptr;  // 需要手動刪除

自定義刪除器

// 自定義刪除器
auto custom_deleter = [](int* p) {
    std::cout << "Custom deleting: " << *p << std::endl;
    delete p;
};

std::unique_ptr<int, decltype(custom_deleter)> ptr(new int(42), custom_deleter);

std::shared_ptr

特性

  • 共享所有權:多個 shared_ptr 可以同時擁有同一物件
  • 參考計數:透過參考計數管理生命週期
  • 執行緒安全:參考計數操作是原子的
  • 控制塊:額外的記憶體存儲參考計數資訊

基本用法

#include <memory>

// 建立 shared_ptr
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2(new int(42));  // 較不推薦

// 複製(增加參考計數)
std::shared_ptr<int> ptr3 = ptr1;
std::cout << "參考計數: " << ptr1.use_count() << std::endl;  // 輸出 2

// 重置(減少參考計數)
ptr3.reset();
std::cout << "參考計數: " << ptr1.use_count() << std::endl;  // 輸出 1

// 檢查是否唯一
if (ptr1.unique()) {
    std::cout << "ptr1 是唯一的擁有者" << std::endl;
}

循環引用問題

// 問題:循環引用導致記憶體洩漏
struct Node {
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;  // 循環引用!
};

// 解決方案:使用 weak_ptr 打破循環
struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;    // 使用 weak_ptr
};

完整範例程式

#include <iostream>
#include <memory>
#include <vector>

class Resource {
public:
    Resource(int id) : id_(id) {
        std::cout << "Resource " << id_ << " created\n";
    }
    
    ~Resource() {
        std::cout << "Resource " << id_ << " destroyed\n";
    }
    
    void use() {
        std::cout << "Using resource " << id_ << "\n";
    }
    
private:
    int id_;
};

void unique_ptr_demo() {
    std::cout << "\n=== unique_ptr 示範 ===\n";
    
    // 創建和使用
    auto ptr1 = std::make_unique<Resource>(1);
    ptr1->use();
    
    // 移動所有權
    auto ptr2 = std::move(ptr1);
    // ptr1 現在是 nullptr
    
    if (!ptr1) {
        std::cout << "ptr1 已失效\n";
    }
    
    ptr2->use();
}

void shared_ptr_demo() {
    std::cout << "\n=== shared_ptr 示範 ===\n";
    
    // 創建和共享
    auto ptr1 = std::make_shared<Resource>(2);
    std::cout << "參考計數: " << ptr1.use_count() << "\n";
    
    {
        auto ptr2 = ptr1;  // 複製
        std::cout << "複製後參考計數: " << ptr1.use_count() << "\n";
        ptr2->use();
    }  // ptr2 銷毀
    
    std::cout << "ptr2 銷毀後參考計數: " << ptr1.use_count() << "\n";
    ptr1->use();
}

int main() {
    unique_ptr_demo();
    shared_ptr_demo();
    
    std::cout << "\n程式結束\n";
    return 0;
}

選擇指南

使用 unique_ptr 當:

  • ✅ 資源只需要一個擁有者
  • ✅ 需要最佳效能(零開銷)
  • ✅ 表達獨佔所有權的語意
  • ✅ 替代原始指標和手動 delete
  • ✅ 在容器中存儲獨有物件

使用 shared_ptr 當:

  • ✅ 多個物件需要共享同一資源
  • ✅ 不確定哪個物件最後使用資源
  • ✅ 需要在不同執行緒間共享資源
  • ✅ 實現觀察者模式或回調機制
  • ✅ 需要延遲清理或弱引用

最佳實踐

  1. 預設使用 unique_ptr:大多數情況下不需要共享
  2. 優先使用 make_unique/make_shared:更安全且效率更高
  3. 避免循環引用:使用 weak_ptr 打破循環
  4. 不要混用:避免將 unique_ptr 轉換為 shared_ptr
  5. 考慮效能:shared_ptr 在多執行緒環境下有額外開銷

效能比較

操作unique_ptrshared_ptr
建立O(1)O(1)
複製不支援O(1) 但有原子操作
移動O(1)O(1)
銷毀O(1)O(1) 但可能觸發物件銷毀
記憶體使用8 bytes16 bytes + 控制塊

常見陷阱

  1. 循環引用:shared_ptr 無法自動解決循環引用
  2. 效能誤解:shared_ptr 不是萬能的,有額外開銷
  3. 執行緒安全:只有參考計數是執行緒安全的,物件本身不是
  4. 自引用:避免物件持有指向自己的 shared_ptr

結論

智能指標是現代 C++ 的重要工具,能大幅提升程式碼的安全性和可維護性。選擇合適的智能指標類型對於寫出高品質的 C++ 程式碼至關重要。記住:「預設使用 unique_ptr,只有在真正需要共享時才使用 shared_ptr」。

C++ Move 語意完整指南

什麼是 Move 語意?

Move 語意是 C++11 引入的重要特性,用於避免不必要的複製操作,提升程式效能。std::move 本身不會移動任何東西,而是將左值轉換為右值引用,告訴編譯器可以安全地「竊取」物件的資源。

核心概念

基本原理

  • 複製語意:建立物件的完整副本(兩個物件都有完整資料)
  • 移動語意:轉移物件的資源(只有一個物件擁有資料)
  • 右值引用std::move 將左值轉換為右值引用,啟用移動語意

移動 vs 複製

操作複製移動
資源分配新分配記憶體轉移現有資源
效能較慢(需要深拷貝)較快(只改變指標)
原物件狀態保持不變可能變為空狀態
適用場景需要保留原物件不再需要原物件

基本語法和範例

1. 字串移動

#include <iostream>
#include <string>

void string_move_example() {
    std::string str1 = "Hello, World!";
    std::cout << "移動前 str1: " << str1 << std::endl;
    
    // 移動語意
    std::string str2 = std::move(str1);
    
    std::cout << "移動後 str1: '" << str1 << "' (通常為空)" << std::endl;
    std::cout << "移動後 str2: '" << str2 << "'" << std::endl;
}

2. 容器移動

#include <vector>

void vector_move_example() {
    std::vector<int> vec1 = {1, 2, 3, 4, 5};
    std::cout << "移動前 vec1 大小: " << vec1.size() << std::endl;
    
    // 移動整個向量
    std::vector<int> vec2 = std::move(vec1);
    
    std::cout << "移動後 vec1 大小: " << vec1.size() << std::endl;  // 通常為 0
    std::cout << "移動後 vec2 大小: " << vec2.size() << std::endl;  // 5
}

3. 自定義類別的移動

#include <iostream>
#include <string>

class Person {
public:
    Person(std::string name) : name_(std::move(name)) {
        std::cout << "建構: " << name_ << std::endl;
    }
    
    // 複製建構函數
    Person(const Person& other) : name_(other.name_) {
        std::cout << "複製建構: " << name_ << std::endl;
    }
    
    // 移動建構函數
    Person(Person&& other) noexcept : name_(std::move(other.name_)) {
        std::cout << "移動建構: " << name_ << std::endl;
    }
    
    ~Person() {
        std::cout << "解構: " << name_ << std::endl;
    }
    
    const std::string& getName() const { return name_; }
    
private:
    std::string name_;
};

void class_move_example() {
    Person person1("Alice");
    
    // 複製 vs 移動比較
    Person person2 = person1;              // 複製建構
    Person person3 = std::move(person1);   // 移動建構
    
    std::cout << "person1: '" << person1.getName() << "'" << std::endl;
    std::cout << "person2: '" << person2.getName() << "'" << std::endl;
    std::cout << "person3: '" << person3.getName() << "'" << std::endl;
}

實際應用場景

1. 函數參數傳遞

// 接受字串參數的函數
void processString(std::string str) {
    std::cout << "處理: " << str << std::endl;
    // 對字串進行處理...
}

void function_parameter_example() {
    std::string text = "重要資料";
    
    // 方法1: 複製傳遞(text 保持不變)
    processString(text);
    std::cout << "複製後 text: " << text << std::endl;
    
    // 方法2: 移動傳遞(text 可能變空)
    processString(std::move(text));
    std::cout << "移動後 text: '" << text << "'" << std::endl;
}

2. 容器操作

#include <vector>

void container_operations() {
    std::vector<std::string> names;
    
    // 方法1: 複製加入
    std::string name1 = "Alice";
    names.push_back(name1);  // name1 仍然有效
    std::cout << "name1 仍然存在: " << name1 << std::endl;
    
    // 方法2: 移動加入
    std::string name2 = "Bob";
    names.push_back(std::move(name2));  // name2 可能變空
    std::cout << "name2 狀態: '" << name2 << "'" << std::endl;
    
    // 方法3: 直接建構(最有效率)
    names.emplace_back("Charlie");
    
    // 顯示容器內容
    for (const auto& name : names) {
        std::cout << "容器中: " << name << std::endl;
    }
}

3. 智能指標轉移

#include <memory>

void smart_pointer_example() {
    // unique_ptr 只能移動,不能複製
    std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
    std::cout << "ptr1 指向值: " << *ptr1 << std::endl;
    
    // 轉移所有權
    std::unique_ptr<int> ptr2 = std::move(ptr1);
    
    // 檢查狀態
    std::cout << "ptr1 是否為空: " << (ptr1 == nullptr) << std::endl;  // true
    std::cout << "ptr2 指向值: " << *ptr2 << std::endl;                // 42
}

4. 函數返回值優化

#include <vector>

// 返回大型物件
std::vector<int> createLargeVector() {
    std::vector<int> result(1000000, 42);
    return result;  // 編譯器通常會自動優化(RVO/NRVO)
}

// 明確使用移動
std::vector<int> moveVector(std::vector<int> input) {
    // 對 input 進行處理...
    return std::move(input);  // 明確移動
}

void return_value_example() {
    auto vec1 = createLargeVector();     // 高效率
    auto vec2 = moveVector(std::move(vec1));  // 明確移動
    
    std::cout << "vec1 大小: " << vec1.size() << std::endl;  // 可能為 0
    std::cout << "vec2 大小: " << vec2.size() << std::endl;  // 1000000
}

進階技巧

1. 完美轉發 (Perfect Forwarding)

#include <utility>

template<typename T>
void wrapper(T&& arg) {
    // 使用 std::forward 保持參數的值類別
    actual_function(std::forward<T>(arg));
}

2. 移動賦值運算子

class MyClass {
public:
    // 移動賦值運算子
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            // 釋放當前資源
            // 移動 other 的資源
            data_ = std::move(other.data_);
        }
        return *this;
    }
    
private:
    std::string data_;
};

3. 條件移動

template<typename T>
void conditionalMove(T&& value, bool shouldMove) {
    if (shouldMove) {
        process(std::move(value));
    } else {
        process(value);  // 複製
    }
}

常見陷阱和注意事項

1. 移動後不要使用原物件

std::string str = "Hello";
std::string moved = std::move(str);
// 危險:str 可能已經無效
// std::cout << str << std::endl;  // 可能輸出空字串或未定義行為

2. 不要移動 const 物件

const std::string str = "Hello";
// std::move(str);  // 無效果,const 物件不能被移動

3. 返回值不需要明確 move

std::string func() {
    std::string result = "Hello";
    return result;  // 編譯器會自動優化,不需要 std::move(result)
}

4. 小物件移動可能沒有優勢

// 對於 int, char 等小型物件,移動和複製效能相似
int a = 42;
int b = std::move(a);  // 沒有明顯優勢

效能比較

測試範例

#include <chrono>
#include <vector>

void performance_test() {
    // 建立大型容器
    std::vector<int> large_vector(1000000, 42);
    
    // 測試複製
    auto start = std::chrono::high_resolution_clock::now();
    std::vector<int> copied = large_vector;
    auto end = std::chrono::high_resolution_clock::now();
    
    auto copy_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    
    // 測試移動
    start = std::chrono::high_resolution_clock::now();
    std::vector<int> moved = std::move(large_vector);
    end = std::chrono::high_resolution_clock::now();
    
    auto move_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    
    std::cout << "複製耗時: " << copy_time.count() << " 微秒" << std::endl;
    std::cout << "移動耗時: " << move_time.count() << " 微秒" << std::endl;
}

最佳實踐

1. 何時使用 Move

  • ✅ 不再需要原物件時
  • ✅ 傳遞大型物件給函數
  • ✅ 在容器中插入臨時物件
  • ✅ 實現高效的資源轉移

2. 何時不要使用 Move

  • ❌ 還需要使用原物件
  • ❌ 物件很小(如 int, char)
  • ❌ const 物件
  • ❌ 函數返回值(編譯器會自動優化)

3. 實現移動語意的準則

  • 移動建構函數應該標記為 noexcept
  • 移動後的物件應該處於有效但未指定的狀態
  • 自我賦值要安全處理
  • 資源管理要正確

總結

Move 語意是現代 C++ 的重要特性,能夠:

  • 提升效能:避免不必要的深拷貝
  • 資源管理:高效轉移物件所有權
  • 語意清晰:明確表達資源轉移意圖
  • 記憶體效率:減少記憶體分配和釋放

記住核心原則:當你不再需要某個物件時,使用 std::move 將其資源轉移給其他物件,避免昂貴的複製操作。

完整範例程式

#include <iostream>
#include <string>
#include <vector>
#include <memory>

int main() {
    // 字串移動
    std::string str1 = "Hello, World!";
    std::string str2 = std::move(str1);
    std::cout << "移動後 str1: '" << str1 << "', str2: '" << str2 << "'" << std::endl;
    
    // 容器移動
    std::vector<int> vec1 = {1, 2, 3, 4, 5};
    std::vector<int> vec2 = std::move(vec1);
    std::cout << "移動後 vec1 大小: " << vec1.size() 
              << ", vec2 大小: " << vec2.size() << std::endl;
    
    // 智能指標移動
    auto ptr1 = std::make_unique<int>(42);
    auto ptr2 = std::move(ptr1);
    std::cout << "ptr1 為空: " << (ptr1 == nullptr) 
              << ", ptr2 值: " << *ptr2 << std::endl;
    
    return 0;
}

這段程式碼定義了一個二元樹(Binary Tree)結構,並提供操作和管理樹中節點的功能。主要目的是構建一個完全二元樹,讓用戶可以輸入兩個節點的值來查找它們之間的路徑。下面是程式的詳細分解與說明:


1. 變數 MAX_DEPTH

  • 設定樹的最大深度,這裡設定為 5,代表樹最多可以有 6 層(根節點為第 0 層)。這個設定影響樹的總節點數。

2. Node 類別

  • 定義了樹中每個節點的結構,包括:
    • value: 節點的值(例如 A, B 等)。
    • parent_index: 父節點在節點列表中的索引,用於找出上一層節點。
    • is_right: 是否是右子節點(0 表示左子節點,1 表示右子節點)。
    • depth: 節點的深度(層數),根節點的深度為 0。
    • path: 使用位運算儲存節點的路徑,用來表示從根節點到當前節點的左右走向。

在這個程式中,path 使用位運算來表示節點從根節點到該節點的左右走向,方便在樹中存儲節點路徑。每個節點的 path 是一個整數,該整數的二進位表示了從根節點走到該節點時的左右移動方向(左為 0,右為 1)。例如,如果我們有一個完全二元樹,從根節點 A 開始,並構建以下結構:

       A
     /   \
    B     C
   / \   / \
  D   E F   G

我們可以分別查看每個節點的 path 值:

例子說明

假設每次走向左子節點為 0、右子節點為 1。節點 path 值的計算會是這樣:

  1. 節點 A (根節點)

    • Apath0,因為它是根節點,不需要任何移動。
  2. 節點 B (A 的左子節點)

    • AB 走左邊,因此 Bpath0(二進位 0)。
  3. 節點 C (A 的右子節點)

    • AC 走右邊,因此 Cpath1(二進位 1)。
  4. 節點 D (B 的左子節點)

    • AB 再到 D,走向為「左 -> 左」,因此 Dpath00(二進位 00,十進位是 0)。
  5. 節點 E (B 的右子節點)

    • AB 再到 E,走向為「左 -> 右」,因此 Epath01(二進位 01,十進位是 1)。
  6. 節點 F (C 的左子節點)

    • AC 再到 F,走向為「右 -> 左」,因此 Fpath10(二進位 10,十進位是 2)。
  7. 節點 G (C 的右子節點)

    • AC 再到 G,走向為「右 -> 右」,因此 Gpath11(二進位 11,十進位是 3)。

path 值總結

節點路徑方向path (二進位)path (十進位)
A-00
B00
C11
D左 -> 左000
E左 -> 右011
F右 -> 左102
G右 -> 右113

path 值的用途

在二元樹中,path 可以被用來快速確定從根到任意節點的走向。例如,透過判斷 path 的二進位值中的每個位,可以知道應該向左還是向右移動。


3. BinaryTree 類別

這是管理整個二元樹的核心類別,負責樹的建立、節點的添加、路徑查找等功能。主要成員包括:

  • nodes: 用於儲存所有節點的列表。
  • size: 樹中當前節點的數量。
  • value_to_index: 將節點值對應到節點索引的字典,便於查找節點索引。

(1) add_node 方法

  • 用來添加新節點。若為根節點,則 parent_idx 設為 -1,depth 設為 0。
  • 否則,根據父節點的深度和路徑來計算新節點的 depthpath
  • 在節點列表 nodes 中新增這個節點,並更新 value_to_index 字典。

(2) find_node_index 方法

  • 查找特定值的節點索引,如果找不到,則回傳 -1。

(3) find_lca_index 方法

  • 查找兩個節點的最近公共祖先(Lowest Common Ancestor, LCA)。
  • 使用迴圈,使兩個節點逐層往上尋找,直到它們相遇為止。

(4) generate_path 方法

  • 根據兩個節點之間的關係生成路徑字符串:
    • 從起始節點一路向上到最近公共祖先(LCA),路徑為 "上"
    • 從 LCA 開始,沿著左右走向到達目標節點,路徑為 "左""右"
    • 最後合併這兩部分路徑,並回傳最終的路徑字串。

(5) find_path_between_nodes 方法

  • 查找指定節點之間的路徑,顯示從 start_valend_val 的路徑。
  • 若節點不存在,會提示使用者。

4. main() 函數

  • 用來建立並測試二元樹的主程式。
  • 以完全二元樹結構建立節點,節點的值從 A 開始,並依序增加。
  • 使用 deque 進行廣度優先遍歷,將父節點依次出列,再添加其左右子節點(節點值字母超過 Z 時,進入 AA, AB 等雙字母模式)。
  • 最後提供互動式功能,讓使用者輸入兩個節點的值來查找並顯示它們之間的路徑。

執行流程

  1. BinaryTree 類別被初始化,並建立根節點。
  2. 利用 deque 來按層構建完全二元樹,直到達到最大節點數量為止。
  3. 用戶可以輸入兩個節點的值來查找它們之間的路徑。程式會利用最近公共祖先(LCA)來生成路徑,顯示從起始節點到目標節點的走向。

例子

假設樹的結構如下(僅展示部分):

       A
     /   \
    B     C
   / \   / \
  D   E F   G

若使用者輸入從 DG 的路徑:

  • 程式會找到 DG 的最近公共祖先 A
  • DA 的路徑為 "上上"
  • AG 的路徑為 "右左"
  • 結果輸出 "上上右左"

這段程式提供了有效的方法來構建完全二元樹,並以位運算儲存節點路徑,方便查找節點間的路徑。


# 定義樹的最大深度
MAX_DEPTH = 5  # 為了方便展示,這里設為5,可以根據需要調整


# 輔助函數:生成節點名稱
def get_next_node_value(index):
    letters = []
    while index >= 0:
        letters.append(chr(index % 26 + ord("A")))
        index = index // 26 - 1
    return "".join(reversed(letters))


# 節點類,表示樹中的每個節點
class Node:
    def __init__(self, value, parent_index, is_right, depth, path):
        self.value = value  # 節點值
        self.parent_index = parent_index  # 父節點索引
        self.is_right = is_right  # 標記是左子節點還是右子節點
        self.depth = depth  # 節點深度
        self.path = path  # 使用位運算表示的路徑


# 二元樹類,用於管理樹
class BinaryTree:
    def __init__(self):
        self.nodes = []  # 節點列表
        self.size = 0  # 當前節點數量
        self.value_to_index = {}  # 節點值到索引的映射

    # 向樹中添加節點
    def add_node(self, value, parent_idx, is_right):
        print(value)
        if self.size >= (1 << (MAX_DEPTH + 1)):
            return -1

        if parent_idx == -1:
            depth = 0
            path = 0
        else:
            depth = self.nodes[parent_idx].depth + 1
            if depth >= 64:  # 使用64位整數
                print("深度超過限制")
                return -1
            path = self.nodes[parent_idx].path | (is_right << depth)

        node = Node(value, parent_idx, is_right, depth, path)
        self.nodes.append(node)
        curr_idx = self.size
        self.size += 1

        # 更新節點值到索引的映射
        self.value_to_index[value] = curr_idx

        return curr_idx

    # 查找具有給定值的節點的索引
    def find_node_index(self, value):
        return self.value_to_index.get(value, -1)

    # 利用 path 和 depth 找到最近公共祖先的索引
    def find_lca_index(self, idx1, idx2):
        node1, node2 = self.nodes[idx1], self.nodes[idx2]
        while node1.path != node2.path:
            if node1.depth > node2.depth:
                idx1 = node1.parent_index
                node1 = self.nodes[idx1]
            elif node2.depth > node1.depth:
                idx2 = node2.parent_index
                node2 = self.nodes[idx2]
            else:
                idx1, idx2 = node1.parent_index, node2.parent_index
                node1, node2 = self.nodes[idx1], self.nodes[idx2]
        return idx1

    # 使用 path 屬性生成從 start_idx 到 end_idx 的路徑字符串
    def generate_path(self, start_idx, end_idx):
        if start_idx == end_idx:
            return ""

        # 找到最近公共祖先的索引
        lca_idx = self.find_lca_index(start_idx, end_idx)

        path_parts = []

        # 從起始節點向上到LCA
        current_idx = start_idx
        while current_idx != lca_idx:
            path_parts.append("上")
            current_idx = self.nodes[current_idx].parent_index

        # 從LCA向下到目標節點
        directions = []
        current_idx = end_idx
        while current_idx != lca_idx:
            node = self.nodes[current_idx]
            if node.is_right:
                directions.append("右")
            else:
                directions.append("左")
            current_idx = node.parent_index

        path_parts.extend(reversed(directions))
        return "".join(path_parts)

    # 查找兩個節點之間的路徑並輸出
    def find_path_between_nodes(self, start_val, end_val):
        start_idx = self.find_node_index(start_val)
        end_idx = self.find_node_index(end_val)

        if start_idx == -1 or end_idx == -1:
            print("節點不存在")
            return

        path = self.generate_path(start_idx, end_idx)
        print(f"從 {start_val} 到 {end_val} 的路徑: {path}")


# 主函數,測試二元樹功能
def main():
    tree = BinaryTree()

    # 建立完全二元樹
    from collections import deque

    max_nodes = 2 ** (MAX_DEPTH + 1) - 1  # 完全二元樹的節點總數
    node_queue = deque()
    value_ord = 0  # 節點索引從0開始,將被轉換為字母表示

    # 添加根節點
    root_value = get_next_node_value(value_ord)
    root_idx = tree.add_node(root_value, -1, 0)
    node_queue.append((root_idx, value_ord))
    value_ord += 1

    while node_queue and tree.size < max_nodes:
        parent_idx, parent_value_ord = node_queue.popleft()

        # 添加左子節點
        left_value = get_next_node_value(value_ord)
        left_idx = tree.add_node(left_value, parent_idx, 0)
        node_queue.append((left_idx, value_ord))
        value_ord += 1

        # 添加右子節點
        right_value = get_next_node_value(value_ord)
        right_idx = tree.add_node(right_value, parent_idx, 1)
        node_queue.append((right_idx, value_ord))
        value_ord += 1

    # 提供交互方式,允許用戶輸入任意兩個節點的值
    print("請輸入要查找路徑的節點,輸入 'exit' 退出程序。")
    while True:
        try:
            start_val = input("請輸入起始節點: ")
            if start_val.lower() == "exit":
                break
            end_val = input("請輸入目標節點: ")
            if end_val.lower() == "exit":
                break
            tree.find_path_between_nodes(start_val.strip(), end_val.strip())
        except KeyboardInterrupt:
            print("\n程序已退出。")
            break
        except Exception as e:
            print(f"發生錯誤: {e}")


if __name__ == "__main__":
    main()

find_lca_index 函數用於找到二元樹中兩個節點的最近公共祖先(LCA, Lowest Common Ancestor),即兩個節點共同的最深祖先節點。

基本邏輯

  1. 比較兩個節點的 path 屬性,若不同則逐步向上回溯,直到兩者的 path 一致,即找到共同祖先。
  2. 如果兩個節點深度不同,較深的節點會先回溯到淺層,直至兩者深度相同。
  3. 如果兩者深度相同且路徑不同,則同時向上回溯父節點,直到路徑相同。

示例

假設一棵二元樹的結構如下,根節點為 A,左右子節點分別依序命名為 B, C, D, E, F, G

         A
       /   \
      B     C
     / \   / \
    D   E F   G

示例:找節點 DG 的最近公共祖先

  1. D 的索引為 3G 的索引為 6
  2. 使用 find_lca_index(3, 6) 時,首先比較兩個節點的 path 值。
  3. DG 的深度相同,但 path 不同,因此兩者同時回溯到其父節點,分別是 BC
  4. 接下來,BC 的深度相同,但仍然 path 不同,繼續回溯到其父節點 A
  5. ADG 的最近公共祖先,因此返回 A 的索引 0

此過程確認 find_lca_index 可以通過路徑和深度找到最靠近根的公共祖先。

當我們使用 find_lca_index 函數時,會利用每個節點的 path 值來加速找到兩個節點的最近公共祖先(LCA)。以下是一個完整說明,包括 path 值的計算方式、範例,以及加上 path 後提高效能的原因。

path 屬性的計算方式

path 屬性是一個整數,使用位運算來表示從根節點到當前節點的路徑:

  • 根節點的 path0
  • 對於每個子節點,若為左子節點,則 path 保持不變;若為右子節點,則將 path 的第 depth 位設為 1
  • 這樣,每個節點的 path 就可以唯一地表示從根節點出發的路徑。

例如:

  1. 根節點 Apath = 0
  2. A 的左子節點 B 仍保持 path = 0(因為是左子節點)。
  3. A 的右子節點 Cpath1(因為是右子節點)。
  4. B 的左子節點 D 繼承 Bpath = 0
  5. B 的右子節點 Epath10(即 2,因為是 B 的右子節點,在第 2 位設為 1)。
  6. 同理,C 的左子節點 Fpath01(即 1),C 的右子節點 Gpath11(即 3)。

樹結構的節點及其對應 path 值:

         A (path=0)
       /       \
   B (path=0)   C (path=1)
     /   \       /     \
D (0)   E (2)  F (1)   G (3)

find_lca_index 的示例與過程

假設要找 DG 的最近公共祖先:

  1. D 的索引為 3path0,深度為 2
  2. G 的索引為 6path3,深度也為 2

我們在 find_lca_index 中的比較過程為:

  1. DGpath 不同,但深度相同,因此同時回溯到各自的父節點,分別是 BC
  2. BCpath 仍然不同,繼續回溯到 A
  3. A 處,path 相同(均為 0),因此 ADG 的最近公共祖先,返回 A 的索引 0

使用 path 提高效能的原因

傳統方法需要沿樹逐層回溯父節點以找到公共祖先,而 path 可以壓縮多層回溯的操作,提供更快速的判斷:

  • path 用位元表示樹路徑,對比節點只需判斷 path 值是否一致,大幅減少比較操作。
  • path 不同且深度一致時,可以一次性向上找到公共祖先,而不必多層次逐步上溯。

這種方法利用了整數位元的快速比較特性,有效地優化了查找的速度,特別是在深度較大的樹中,可以顯著提升查找效率。

# 查找最近公共祖先的索引 **第一版**:
def find_lca_index(self, idx1, idx2):
    node1_idx = idx1
    node2_idx = idx2

    node1 = self.nodes[node1_idx]
    node2 = self.nodes[node2_idx]

    while node1_idx != node2_idx:
        if node1.depth > node2.depth:
            node1_idx = node1.parent_index
            node1 = self.nodes[node1_idx]
        elif node2.depth > node1.depth:
            node2_idx = node2.parent_index
            node2 = self.nodes[node2_idx]
        else:
            node1_idx = node1.parent_index
            node2_idx = node2.parent_index
            node1 = self.nodes[node1_idx]
            node2 = self.nodes[node2_idx
# 利用 path 和 depth 找到最近公共祖先的索引  **第二版**:
def find_lca_index(self, idx1, idx2):
    # 獲取兩個節點的初始節點對象
    node1, node2 = self.nodes[idx1], self.nodes[idx2]

    # 當兩個節點的 path 不相等時,進行回溯以找到公共祖先
    while node1.path != node2.path:
        # 如果 node1 深度大於 node2,則將 node1 向上移動至父節點
        if node1.depth > node2.depth:
            idx1 = node1.parent_index
            node1 = self.nodes[idx1]
        # 如果 node2 深度大於 node1,則將 node2 向上移動至父節點
        elif node2.depth > node1.depth:
            idx2 = node2.parent_index
            node2 = self.nodes[idx2]
        # 如果兩個節點的深度相同,則同時向上移動到各自的父節點
        else:
            idx1, idx2 = node1.parent_index, node2.parent_index
            node1, node2 = self.nodes[idx1], self.nodes[idx2]

    # 當 node1 和 node2 的 path 相等時,即找到了最近公共祖先
    return idx1

這裡是完整的 15 個節點樹狀結構 path 值表示,其中每個節點的 path 值根據從根節點 A 移動的方向來計算。0 表示向左移動,1 表示向右移動。

節點路徑方向path (二進位)path (十進位)
A-00
B00
C11
D左 -> 左000
E左 -> 右011
F右 -> 左102
G右 -> 右113
H左 -> 左 -> 左0000
I左 -> 左 -> 右0011
J左 -> 右 -> 左0102
K左 -> 右 -> 右0113
L右 -> 左 -> 左1004
M右 -> 左 -> 右1015
N右 -> 右 -> 左1106
O右 -> 右 -> 右1117

這些 path 值能夠在樹的深度和節點關係中提供快速查找功能,尤其是使用 path 值進行公共祖先查找時。

在這兩個版本的 find_lca_index 中,主要區別在於條件的檢查方式。第一個版本直接比較兩個節點的索引,利用索引回溯至同一節點;第二個版本則比較兩個節點的 path,而回溯流程上是類似的。

效能分析

  1. 第一版

    • 直接比對索引 (node1_idxnode2_idx),當兩者相同即找到最近公共祖先。當兩者深度不同時,會將較深的節點上移至父節點,否則同時將兩者上移。
    • 效能優點:操作相對簡潔,直接使用索引對比,減少了對 path 的檢查操作。
  2. 第二版

    • 比較兩個節點的 path,當 path 不同時才進行回溯操作。
    • 效能優點:當樹具有多層且每層節點密集時,path 可以有效縮短查找最近公共祖先的步數。因為如果 path 不同,說明兩個節點在不同的分支中,能有效地排除分支差異,從而更快定位到共同的祖先。

效能結論

第二版使用 path 來進行篩選,尤其在多層大樹結構中,能夠減少冗餘的節點回溯數量,因此 在深層且節點數量多的樹結構中,會更具效能優勢

支援 Python 語法、又有 C 速度的新程式語言 Mojo 環境配置筆記

介紹

Mojo 是一名比較新的語言,是由 LLVM 之父和 Swift 之父 Chris Lattner 所開發。

之所以開發 Mojo,據稱是為了填補『研究』與『生產』的鴻溝,所以 Mojo 擁有 Python 般簡易的語法以及 C 的執行速度。當然,最主要的可能還是對於 AI 的優化 —— 現在 AI 的市場已經值得一門全新的程式語言了。

當然,Mojo 到底有多少實力、有多少潛力待發掘,這個疑問只能留待日後了。總之我今天就來試用看看這個很紅的新程式語言吧!

一方面也是因為 Mojo SDK 現在已經支援 Ubuntu Linux 系統了(2023/09/13),至於 Windows 和 MacOS 等作業系統則可能還需要等上一段時間。


取得 Mojo SDK

首先,確認滿足系統需求。

System requirements To use the Mojo SDK, you need a system that meets these specifications:

  • Ubuntu 20.04/22.04 LTS
  • x86-64 CPU (with SSE4.2 or newer) and a minimum of 8 GiB memory
  • Python 3.8 – 3.10
  • g++ or clang++ C++ compiler

確認為有以上環境後,就可以按照開始配置環境了。

curl https://get.modular.com | \
  MODULAR_AUTH=mut_368ff64729824e39b3413866bf6be10d \
  sh -


modular auth X &&
modular install mojo

如果這個方法不行,可以考慮使用手動安裝。我自己就是使用手動安裝的方式。因為我遇到了:

 ^^^^: ... Failed to update via apt-get update - Context above.
 !!!!: Oh no, your setup failed! :-( ... But we might be able to help. :-)
 !!!!: 
 !!!!: You can contact Modular for further assistance.
 !!!!: 

這樣的奇怪錯誤,之後在網路上查到了 https://github.com/modularml/mojo/issues/574 這個 issue,推薦可以試試手動安裝。

apt-get install -y apt-transport-https &&
  keyring_location=/usr/share/keyrings/modular-installer-archive-keyring.gpg &&
  curl -1sLf 'https://dl.modular.com/bBNWiLZX5igwHXeu/installer/gpg.0E4925737A3895AD.key' |  gpg --dearmor >> ${keyring_location} &&
  curl -1sLf 'https://dl.modular.com/bBNWiLZX5igwHXeu/installer/config.deb.txt?distro=debian&codename=wheezy' > /etc/apt/sources.list.d/modular-installer.list &&
  apt-get update &&
  apt-get install -y modular

modular auth mut_368ff64729824e39b3413866bf6be10d &&
modular install mojo

之後,還要設定 mojo 的路徑。

echo 'export MODULAR_HOME="$HOME/.modular"' >> ~/.bashrc

echo 'export PATH="$MODULAR_HOME/pkg/packages.modular.com_mojo/bin:$PATH"' >> ~/.bashrc

source ~/.bashrc

安裝好後,應該就可以使用 mojo 指令了。 更進一步的指令介紹可以看 Mojo command-line interface (CLI)

第一支程式: Hello World

好久沒有學習新語言了,遵循常見規則,總之先寫下一份 hello.mojo。

fn main():
    print("Hello World!")

接著我們要執行:

mojo hello.mojo

Output:

Hello World!

我們也可以使用 mojo build 來建立靜態連結的二進制檔案。

mojo build hello.mojo

會建立一個同名的 .mojo 執行檔。我們可以像編譯 C++ 檔案一樣使用 -o 來指定特定的輸出名稱。


心得

目前距離 Mojo 釋放出環境跟編譯器還不到一週的時間,看得出來這真的是很新的東西,安裝的時候其實踩了許多坑,都是慢慢看著 GitHub 上的 issues 解掉的。

語法也還需要熟悉,目標是拿 Mojo 來訓練模型,比較看看現在的各種加速方案,看看是不是真的有所幫助。

總之之後來學習下語法吧!

Mojo 爬蟲完整指南

📚 目錄

🚀 Mojo 安裝

系統需求

  • 作業系統: Ubuntu 20.04+ 或 macOS 12+
  • 硬體: x86-64 或 ARM64 架構
  • 記憶體: 至少 4GB RAM

安裝步驟

方法 1: 官方安裝器(推薦)

# 1. 訪問 Modular 官網
curl -s https://get.modular.com | sh -

# 2. 安裝 Mojo
modular install mojo

# 3. 設定環境變數
echo 'export MODULAR_HOME="$HOME/.modular"' >> ~/.bashrc
echo 'export PATH="$MODULAR_HOME/pkg/packages.modular.com_mojo/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# 4. 驗證安裝
mojo --version

方法 2: MAX Platform

# 1. 註冊並下載 MAX Platform
# 2. 安裝 MAX
sudo dpkg -i max-*.deb

# 3. 啟動 MAX
max auth login

# 4. 安裝 Mojo
max install mojo

# 5. 驗證
mojo --version

開發環境設定

VS Code 擴展

# 安裝 Mojo 語言支援
code --install-extension modular-mojotools.mojo

Jupyter Notebook 支援

# 安裝 Jupyter
pip install jupyter

# 註冊 Mojo 核心
max install jupyter

# 啟動 Jupyter
jupyter notebook

🛠️ 環境準備

Python 依賴安裝

# 安裝爬蟲必要套件
pip install requests beautifulsoup4 lxml html5lib aiohttp

# 可選套件
pip install selenium pandas numpy

項目結構

mojo-crawler/
├── crawler.mojo          # 主爬蟲檔案
├── utils.mojo           # 工具函數
├── config.mojo          # 配置檔案
├── requirements.txt     # Python 依賴
└── README.md           # 說明文件

🕷️ 基礎爬蟲範例

1. 簡單的網頁爬蟲

# crawler.mojo
from python import Python

def main():
    """基礎爬蟲範例"""
    
    # 導入 Python 模組
    let requests = Python.import_module("requests")
    let bs4 = Python.import_module("bs4")
    
    # 設定請求標頭
    let headers = Python.dict()
    headers["User-Agent"] = "Mozilla/5.0 (compatible; MojoCrawler/1.0)"
    
    try:
        # 發送 HTTP 請求
        let url = "https://example.com"
        let response = requests.get(url, headers=headers, timeout=10)
        
        if response.status_code == 200:
            print("✅ 請求成功")
            
            # 解析 HTML
            let soup = bs4.BeautifulSoup(response.content, "html.parser")
            
            # 提取標題
            let title = soup.find("title")
            if title:
                print("標題:", title.get_text())
            
            # 提取所有連結
            let links = soup.find_all("a", href=True)
            print(f"找到 {len(links)} 個連結")
            
            for i in range(min(5, len(links))):  # 只顯示前5個
                let link = links[i]
                print(f"  {i+1}. {link.get_text()}: {link['href']}")
                
        else:
            print("❌ 請求失敗, 狀態碼:", response.status_code)
            
    except Exception as e:
        print("錯誤:", e)

2. JSON API 爬蟲

def crawl_json_api():
    """爬取 JSON API 數據"""
    
    let requests = Python.import_module("requests")
    let json = Python.import_module("json")
    
    let headers = Python.dict()
    headers["Accept"] = "application/json"
    headers["User-Agent"] = "MojoCrawler/1.0"
    
    try:
        # 爬取 API 數據
        let api_url = "https://jsonplaceholder.typicode.com/posts"
        let response = requests.get(api_url, headers=headers)
        
        if response.status_code == 200:
            let data = response.json()
            print(f"📊 獲取到 {len(data)} 筆數據")
            
            # 處理前3筆數據
            for i in range(min(3, len(data))):
                let post = data[i]
                print(f"\n📝 貼文 {i+1}:")
                print(f"  標題: {post['title']}")
                print(f"  用戶ID: {post['userId']}")
                print(f"  內容: {post['body'][:50]}...")
                
        else:
            print("❌ API 請求失敗")
            
    except Exception as e:
        print("錯誤:", e)

🚀 進階爬蟲範例

1. 面向對象的爬蟲類

from python import Python
from memory import Reference

struct WebCrawler:
    """高性能網頁爬蟲"""
    var session: PythonObject
    var headers: PythonObject
    var delay: Float64
    var max_retries: Int
    
    fn __init__(inout self):
        """初始化爬蟲"""
        let requests = Python.import_module("requests")
        self.session = requests.Session()
        
        # 設定標頭
        self.headers = Python.dict()
        self.headers["User-Agent"] = "Mozilla/5.0 (compatible; MojoCrawler/2.0)"
        self.headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
        self.headers["Accept-Language"] = "zh-TW,zh;q=0.9,en;q=0.8"
        self.headers["Accept-Encoding"] = "gzip, deflate, br"
        self.headers["Connection"] = "keep-alive"
        
        self.delay = 1.0
        self.max_retries = 3
    
    fn fetch_url(self, url: String) raises -> PythonObject:
        """獲取 URL 內容(帶重試機制)"""
        let time = Python.import_module("time")
        
        for retry in range(self.max_retries):
            try:
                let response = self.session.get(
                    url, 
                    headers=self.headers, 
                    timeout=10,
                    allow_redirects=True
                )
                
                if response.status_code == 200:
                    return response
                elif response.status_code == 429:  # Too Many Requests
                    print(f"⚠️ 請求過於頻繁,等待 {(retry + 1) * 2} 秒...")
                    time.sleep((retry + 1) * 2)
                else:
                    print(f"❌ HTTP {response.status_code}")
                    
            except Exception as e:
                print(f"🔄 重試 {retry + 1}/{self.max_retries}: {e}")
                if retry < self.max_retries - 1:
                    time.sleep(self.delay * (retry + 1))
        
        raise Error("所有重試都失敗了")
    
    fn parse_html(self, html_content: PythonObject) -> PythonObject:
        """解析 HTML 內容"""
        let bs4 = Python.import_module("bs4")
        return bs4.BeautifulSoup(html_content, "html.parser")
    
    fn extract_data(self, soup: PythonObject) -> PythonObject:
        """提取結構化數據"""
        let data = Python.dict()
        
        # 提取標題
        let title = soup.find("title")
        data["title"] = title.get_text().strip() if title else "無標題"
        
        # 提取 meta 描述
        let meta_desc = soup.find("meta", attrs={"name": "description"})
        data["description"] = meta_desc.get("content", "") if meta_desc else ""
        
        # 提取所有標題
        let headings = Python.list()
        for level in range(1, 7):  # h1-h6
            let tags = soup.find_all(f"h{level}")
            for i in range(len(tags)):
                let heading = tags[i]
                headings.append({
                    "level": level,
                    "text": heading.get_text().strip()
                })
        data["headings"] = headings
        
        # 提取連結
        let links = Python.list()
        let link_tags = soup.find_all("a", href=True)
        for i in range(len(link_tags)):
            let link = link_tags[i]
            links.append({
                "text": link.get_text().strip(),
                "url": link["href"]
            })
        data["links"] = links
        
        # 提取圖片
        let images = Python.list()
        let img_tags = soup.find_all("img", src=True)
        for i in range(len(img_tags)):
            let img = img_tags[i]
            images.append({
                "src": img["src"],
                "alt": img.get("alt", "")
            })
        data["images"] = images
        
        return data

2. 批量爬蟲範例

def batch_crawl_demo():
    """批量爬蟲示例"""
    
    let time = Python.import_module("time")
    let json = Python.import_module("json")
    
    var crawler = WebCrawler()
    
    # 要爬取的 URL 列表
    let urls = [
        "https://example.com",
        "https://httpbin.org/html",
        "https://httpbin.org/json"
    ]
    
    let results = Python.list()
    
    print("🚀 開始批量爬取...")
    
    for i in range(len(urls)):
        let url = urls[i]
        print(f"\n📄 正在處理第 {i+1}/{len(urls)} 個: {url}")
        
        try:
            # 獲取頁面
            let response = crawler.fetch_url(url)
            
            # 解析內容
            if "application/json" in str(response.headers.get("content-type", "")):
                # JSON 數據
                let data = response.json()
                results.append({
                    "url": url,
                    "type": "json",
                    "data": data
                })
            else:
                # HTML 數據
                let soup = crawler.parse_html(response.content)
                let extracted_data = crawler.extract_data(soup)
                results.append({
                    "url": url,
                    "type": "html",
                    "data": extracted_data
                })
            
            print("✅ 處理完成")
            
        except Exception as e:
            print(f"❌ 處理失敗: {e}")
            results.append({
                "url": url,
                "type": "error",
                "error": str(e)
            })
        
        # 請求間隔
        if i < len(urls) - 1:
            time.sleep(crawler.delay)
    
    # 保存結果
    try:
        with open("crawl_results.json", "w", encoding="utf-8") as f:
            json.dump(results, f, ensure_ascii=False, indent=2)
        print("\n💾 結果已保存到 crawl_results.json")
    except Exception as e:
        print(f"💥 保存失敗: {e}")
    
    print(f"\n🎉 批量爬取完成! 共處理 {len(urls)} 個 URL")

🛠️ 實用工具函數

1. URL 工具

def normalize_url(base_url: String, relative_url: String) -> String:
    """規範化 URL"""
    let urllib = Python.import_module("urllib.parse")
    return str(urllib.urljoin(base_url, relative_url))

def is_valid_url(url: String) -> Bool:
    """檢查 URL 是否有效"""
    let urllib = Python.import_module("urllib.parse")
    let parsed = urllib.urlparse(url)
    return bool(parsed.netloc and parsed.scheme)

2. 數據處理工具

def clean_text(text: PythonObject) -> String:
    """清理文本數據"""
    let re = Python.import_module("re")
    
    # 移除多餘空白
    cleaned = re.sub(r'\s+', ' ', str(text))
    
    # 移除特殊字符
    cleaned = re.sub(r'[^\w\s\u4e00-\u9fff]', '', cleaned)
    
    return str(cleaned).strip()

def extract_emails(text: String) -> PythonObject:
    """提取電子郵件地址"""
    let re = Python.import_module("re")
    let pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
    return re.findall(pattern, text)

def extract_phone_numbers(text: String) -> PythonObject:
    """提取電話號碼(台灣格式)"""
    let re = Python.import_module("re")
    let patterns = [
        r'09\d{8}',           # 手機號碼
        r'0\d{1,2}-\d{7,8}',  # 市話
        r'\(\d{2,3}\)\d{7,8}' # 括號格式
    ]
    
    let results = Python.list()
    for pattern in patterns:
        let matches = re.findall(pattern, text)
        results.extend(matches)
    
    return results

3. 數據存儲工具

def save_to_csv(data: PythonObject, filename: String):
    """保存數據到 CSV"""
    let pandas = Python.import_module("pandas")
    
    try:
        let df = pandas.DataFrame(data)
        df.to_csv(filename, index=False, encoding='utf-8-sig')
        print(f"💾 數據已保存到 {filename}")
    except Exception as e:
        print(f"💥 CSV 保存失敗: {e}")

def save_to_json(data: PythonObject, filename: String):
    """保存數據到 JSON"""
    let json = Python.import_module("json")
    
    try:
        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"💾 數據已保存到 {filename}")
    except Exception as e:
        print(f"💥 JSON 保存失敗: {e}")

📋 最佳實踐

1. 尊重 robots.txt

def check_robots_txt(base_url: String) -> Bool:
    """檢查 robots.txt"""
    let urllib = Python.import_module("urllib.robotparser")
    
    let robots_url = base_url.rstrip('/') + '/robots.txt'
    let rp = urllib.RobotFileParser()
    rp.set_url(robots_url)
    
    try:
        rp.read()
        return rp.can_fetch('*', base_url)
    except:
        return True  # 如果無法讀取,假設允許

2. 請求限制

struct RateLimiter:
    """請求速率限制器"""
    var last_request_time: Float64
    var min_interval: Float64
    
    fn __init__(inout self, requests_per_second: Float64):
        self.last_request_time = 0.0
        self.min_interval = 1.0 / requests_per_second
    
    fn wait_if_needed(inout self):
        """如有需要則等待"""
        let time = Python.import_module("time")
        let current_time = float(time.time())
        
        let time_since_last = current_time - self.last_request_time
        if time_since_last < self.min_interval:
            let wait_time = self.min_interval - time_since_last
            time.sleep(wait_time)
        
        self.last_request_time = float(time.time())

3. 錯誤處理

def robust_crawl(url: String) -> PythonObject:
    """具有強健錯誤處理的爬蟲函數"""
    let requests = Python.import_module("requests")
    let time = Python.import_module("time")
    
    let max_retries = 3
    let backoff_factor = 2
    
    for attempt in range(max_retries):
        try:
            let response = requests.get(
                url,
                timeout=10,
                headers={"User-Agent": "MojoCrawler/1.0"}
            )
            
            if response.status_code == 200:
                return response
            elif response.status_code == 429:
                let wait_time = backoff_factor ** attempt
                print(f"⏳ 請求限制,等待 {wait_time} 秒...")
                time.sleep(wait_time)
            else:
                print(f"❌ HTTP {response.status_code}")
                
        except Exception as e:
            print(f"🔄 嘗試 {attempt + 1}: {e}")
            if attempt < max_retries - 1:
                time.sleep(backoff_factor ** attempt)
    
    raise Error("所有嘗試都失敗了")

💡 常見問題

Q: Mojo 相比 Python 有什麼優勢?

A: Mojo 在爬蟲方面的優勢:

  • 性能: 比 Python 快 10-100 倍
  • 記憶體效率: 更好的記憶體管理
  • 並行處理: 原生支援並行計算
  • 兼容性: 可以直接使用 Python 套件

Q: 如何處理反爬蟲機制?

A: 常見策略:

# 1. 隨機 User-Agent
let user_agents = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"
]

# 2. 使用代理
let proxies = {
    "http": "http://proxy:port",
    "https": "https://proxy:port"
}

# 3. 模擬真實行為
time.sleep(random.uniform(1, 3))

Q: 如何處理 JavaScript 渲染的頁面?

A: 使用 Selenium:

def crawl_js_page(url: String) -> PythonObject:
    let selenium = Python.import_module("selenium")
    let webdriver = selenium.webdriver
    
    let driver = webdriver.Chrome()
    driver.get(url)
    
    # 等待頁面載入
    let time = Python.import_module("time")
    time.sleep(3)
    
    let content = driver.page_source
    driver.quit()
    
    return content

Q: 如何進行分散式爬蟲?

A: 可以結合 Celery 或 RQ:

# 任務分發
def distribute_urls(urls: PythonObject, num_workers: Int) -> PythonObject:
    let chunks = Python.list()
    let chunk_size = len(urls) // num_workers
    
    for i in range(num_workers):
        let start = i * chunk_size
        let end = start + chunk_size if i < num_workers - 1 else len(urls)
        chunks.append(urls[start:end])
    
    return chunks

🎯 完整範例運行

# 1. 創建項目目錄
mkdir mojo-crawler && cd mojo-crawler

# 2. 創建並運行爬蟲
echo '# 上面的完整代碼' > crawler.mojo
mojo crawler.mojo

# 3. 查看結果
cat crawl_results.json

📚 進階學習資源


注意: 請務必遵守目標網站的使用條款和 robots.txt 規則,進行合理合法的數據收集。

RISC-V 指令集分析

出處:https://ithelp.ithome.com.tw/articles/10257457

有了基本檔案架構後,開始動工指令的部分。RISC-V將指令分成數個子集,其中包括RV32I、RV32E、RV64I、RV128I四套整數指令集,以及約14套擴充指令集,雖然號稱"精簡",最基本的RV32I指令集也有47條指令,實作的effort也是不小。為了能有的放矢的進行實作,目前打算先以實際編譯出來最常用的指令開始處理。

為了觀察實際上compiler常用的指令,需要先取得RISC-V的GNU toolchain,由於目前只打算支援riscv32架構,因此使用的是riscv32-elf-ubuntu-20.04-nightly-2021.06.26-nightly.tar.gz這個版本的toolchain。解壓縮後在riscv/bin資料夾下就可以找到gcc、objdump等常用工具。首先撰寫一個最基本的C程式:

int main() {
    return 0;
}

使用gcc編譯,並使用odjbump得到完整的assembly:

riscv/bin/riscv32-unknown-elf-gcc -o main.elf main.c
riscv/bin/riscv32-unknown-elf-objdump -D -M no-aliases main.elf > main.asm

觀察入口點_start的assembly:

00010084 <_start>:
   10084:   00002197            auipc   gp,0x2
   10088:   b5c18193            addi    gp,gp,-1188 # 11be0 <__global_pointer$>
   1008c:   c3418513            addi    a0,gp,-972 # 11814 <completed.1>
   10090:   c5018613            addi    a2,gp,-944 # 11830 <__BSS_END__>
   10094:   8e09                    c.sub   a2,a0
   10096:   4581                    c.li    a1,0
   10098:   2209                    c.jal   1019a <memset>
   1009a:   00000517            auipc   a0,0x0
   1009e:   26650513            addi    a0,a0,614 # 10300 <atexit>
   100a2:   c511                    c.beqz  a0,100ae <_start+0x2a>
   100a4:   00000517            auipc   a0,0x0
   100a8:   26650513            addi    a0,a0,614 # 1030a <__libc_fini_array>
   100ac:   2c91                    c.jal   10300 <atexit>
   100ae:   2049                    c.jal   10130 <__libc_init_array>
   100b0:   4502                    c.lwsp  a0,0(sp)
   100b2:   004c                    c.addi4spn  a1,sp,4
   100b4:   4601                    c.li    a2,0
   100b6:   2881                    c.jal   10106 <main>
   100b8:   a8b9                    c.j 10116 <exit>

可以發現除了一般的RV32I外,其中為了降低code size大量使用了c.開頭的壓縮指令標準擴充(Standard Extension for Compressed Instructions),因此也必須實作擴充指令集C。

至此已經有一個明顯的目標,就是將此ELF檔能順利跑完,並且能正確的更新所有的整數register以及PC。為達成此目標,指令實作的順序就以從_start開始執行的指令為準。

在QEMU上執行64 bit RISC-V Linux

出處: https://medium.com/swark/%E5%9C%A8qemu%E4%B8%8A%E5%9F%B7%E8%A1%8C64-bit-risc-v-linux-2a527a078819

本篇文章主要是用來記錄我的學習紀錄,嘗試在Virtual box上以Ubuntu 20.04 (kernel版本:5.4.0–54-generic) 來安裝QEMU並執行RISC-V Linux。

先列出本篇文章的重要參考對象 [1]https://ithelp.ithome.com.tw/articles/10192454 [2]https://risc-v-getting-started-guide.readthedocs.io/en/latest/linux-qemu.html [3]https://zhuanlan.zhihu.com/p/258394849

那麼就開始吧 首先我從設定路徑開始 一般的教學都會建議安裝設定如下 $ export RISCV=/opt/riscv 但像我自己是全部安裝在桌面上的資料夾之中 $export RISCV=/home/swark_riscv/Desktop/Riscv *需要注意的是export的路徑,每次重開機後都會消失需要重設,不過也可以修改 "/etc/profile",在其中加入export的環境變數,如此一來重開機之後都會存在。

安裝 riscv-gnu-toolchain

建議先進入github riscv-gnu-toolchain中的README.md上閱讀一下, 可以看到Prerequisites中有提及有些需要先安裝的packages。

像我是使用ubuntu所以必須先用以下指令安裝所需packages

sudo apt-get install autoconf automake autotools-dev curl python3 libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev

接著我們開始安裝riscv-gnu-toolchain

git clone https://github.com/riscv/riscv-gnu-toolchain
cd riscv-gnu-toolchain
git submodule update — init — recursive  //update整包code
./configure --prefix=$RISCV
make linux && make install //安裝Linux ABI(Application binary interface) 專用的toolchain
make && make install //安裝 bare metal 專用的toolchain

*如果是用實體機器來建置環境的人,可以在make後用-j<核心數>,來決定要用幾核來build,比如make -j 4 (表示使用四核心)

安裝完畢後,導出toolcahin的安裝路徑 export PATH=$PATH:$RISCV/bin 順便測試以"riscv64-unknown-linux-gnu-gcc -v"是否安裝成功

img

Busybox的安裝

先退回原工作目錄,比如我是Desktop下的Riscv

git clone https://git.busybox.net/busybox
cd busybox
CROSS_COMPILE=riscv64-unknown-linux-gnu- make menuconfig
CROSS_COMPILE=riscv64-unknown-linux-gnu- make -j 4
CROSS_COMPILE=riscv64-unknown-linux-gnu- make install

make meunconfig中請做以下設定 選擇 Busybox Settings>>Build BusyBox as Static binary 為y

img

*如果遇到以下問題

make menuconfig HOSTCC scripts/kconfig/lxdialog/checklist.o <command-line>: fatal error: curses.h: No such file or directory compilation terminated. make[2]: *** [scripts/Makefile.host:120: scripts/kconfig/lxdialog/checklist.o] Error 1 make[1]: *** [/home/swark_riscv/Desktop/Riscv/busybox-1.27.2/scripts/kconfig/Makefile:14: menuconfig] Error 2 make: *** [Makefile:444: menuconfig] Error 2

我搜尋到的解法是去安裝ncurses,如下,則可解決

sudo apt-get install libncurses5-dev

編譯Linux

退回原工作目錄, 另外目前官方的Linux中已經包含了Risc-V的支援, 因此請直接 git clone https://github.com/torvalds/linux

接著步驟如下

git checkout v5.4 make ARCH=riscv CROSS_COMPILE=$RISCV/bin/riscv64-unknown-linux-gnu- defconfig //配置config
make ARCH=riscv CROSS_COMPILE=$RISCV/bin/riscv64-unknown-linux-gnu- -j 4 //編譯

編譯QEMU

git clone https://github.com/qemu/qemu
cd qemu
git checkout v5.1.0
./configure --target-list=riscv64-softmmu --prefix=$RISCV/qemu
make -j $(nproc)
sudo make install

*可能遭遇問題

ERROR: glib-2.48 gthread-2.0 is required to compile QEMU 
⇒sudo apt-get install libglib2.0-devERROR: pixman >= 0.21.8 not present.
Please install the pixman devel package.
⇒sudo apt-get install libpixman-1-dev

導出Qemu的安裝目錄 export PATH=$PATH:$RISCV/qemu/bin 測試Qemu是否安裝成功

img

製作開機用的rootfs

qemu-img create rootfs.img 1g
mkfs.ext4 rootfs.img
mkdir rootfs
sudo mount -o loop rootfs.img  rootfs
cd rootfs
sudo cp -r ../busyboxsource/_install/* .
sudo mkdir proc sys dev etc etc/init.dcd etc/init.d/
sudo touch rcS
sudo vi rcS

rcS內容如下

#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
/sbin/mdev -s

並且修改rcS權限 sudo chmod +x rcS

最後解除掛載即可 sudo umount rootfs

另外如果是透過[1]的文章去製作rootfs的話,我建議在 cp -r $RISCV/sysroot $RISCV/rootfs中改成 cp -r $RISCV/sysroot $RISCV/rootfs_backup, 並且參考上述步驟,掛載rootfs後再將rootfs_backup內容copy進掛載的目錄中,我最後是透過這個方式才成功。

最後我們可以透過Qemu來執行64 bit RISC-V Linux, 請用以下指令

qemu-system-riscv64 -M virt -m 256M -nographic -kernel linux/arch/riscv/boot/Image -drive file=rootfs.img,format=raw,id=hd0  -device virtio-blk-device,drive=hd0 -append "root=/dev/vda rw console=ttyS0"

img

img


qemu搭建riscv的可偵錯環境

  • riscv工具鏈

(網上大多數用Github直連的工具鏈,但是因為太大,download的時候老是出問題)

選擇使用Cross-compilation toolchains for Linux - Home (bootlin.com)進行下載,之後解壓。

img

bin目錄下為可執行的工具鏈,將其新增到PATH中。

  • qemu

qemu壓縮包下載:QEMU,之後解壓。

默認的安裝命令:

img

這樣會生成qemu支援的所有體系架構的可執行檔案。

如果需要只生成一種架構的,需要組態target-list選項。

img

make之後在build目錄下有對應qemu可執行檔案:

img

將其新增到PATH中。

  • opensbi

(opensbi用於系統啟動程式碼跳轉)

項目github地址:https://github.com/riscv-software-src/opensbi

make時指定交叉編譯器CROSS_COMPILE=riscv64-linux。

同時,指定PLATFORM=generic。

export CROSS_COMPILE=riscv64-linux-
make PLATFORM=generic

img

想要模擬不同類型的裝置,make該項目時的PLATFORM參數可以參考opensbi/docs/platform下的md檔案。

make之後,在opensbi/build/platform/generic/firmware下生成如下檔案:

img

主要有三種類型的firmware:dynamic、jump、payload。

這裡主要使用jump類型的fw_jump.elf檔案,啟動時直接跳轉到OS入口程式碼。

  • linux kernel

直接github上下載linus的分支:torvalds/linux: Linux kernel source tree (github.com)

然後切換到指定版本的tag。

img

make ARCH=riscv CROSS_COMPILE=riscv64-linux- defconfig,之後make ARCH=riscv CROSS_COMPILE=riscv64-linux- menuconfig。

要使用GDB+qemu偵錯核心的話,一般得選中kernel debug以及取消地址隨機化KASLR(不過在riscv相關的組態中沒有發現這個組態)。

img

看riscv社區的新聞:Linux 核心地址空間佈局隨機化 “KASLR” for RISC-V – RISC-V INTERNATIONAL (riscv.org),riscv至今沒有新增該特性。

make之後,會在arch/riscv/boot下生成對應的Image鏡像。

img

  • rootfs

(建立根檔案系統)地址:Buildroot - Making Embedded Linux Easy

make menuconfig選擇RISCV

img

之後sudo make,會在output/images下生成對應的檔案:

img

  • 共享檔案

qemu中運行的虛擬機器往往需要和主機間傳輸資料,因此,最常使用的方式就是共享檔案。

dd if=/dev/zero of=ext4.img bs=512 count=131072
mkfs.ext4 ext4.img
sudo mount -t ext4 -o loop ext4.img ./share

在當前目錄下生成share目錄,可用於虛擬機器和主機間共享資料:

img

  • gdb偵錯
#!/bin/bash

qemu-system-riscv64 -M virt \
	-bios fw_jump.elf \
	-kernel Image \
	-append "rootwait root=/dev/vda ro" \
	-drive file=rootfs.ext2, format=raw,id=hd0 \
	-device virtio-blk-device, device=hd0 \
	-drive file=ext4.img, format=raw,id=hd1 \
	-device virtio-blk-device, driver=hd1 \
	-s -nographic

-s參數使主機端使用連接埠1234進行kernel偵錯。

運行命令後:

img

主機端偵錯,使用riscv64-linux-gdb偵錯編譯kernel後生成的vmlinux。

target remote localhost:1234用於偵錯虛擬機器。

img

以調度的關鍵函數finish_task_switch為斷點為例:

img

此時掛載到從init_task切換到下一個處理程序的過程中了。

常規的gdb命令可以用於偵錯kernel,查看kernel執行階段資訊。

img

img

1 實驗目標

使用GDB在QEMU模擬器中調試運行基於64位RISCV架構的Linux內核。

2 實驗簡介

本次實驗將幫助我們更好地瞭解如何使用交叉編譯工具鏈編譯Linux內核源代碼,並將編譯所得到的基於RISCV指令集架構的Linux內核運行在QEMU模擬器上。本次實驗還將幫助我們更好地瞭解掌握如何利用GDB和QEMU聯合調試內核運行。

關鍵詞: Qemu, Kernel, Linux, OS, RISC-V, Cross Compiler Toolchain, GDB

3 實驗環境

3.1 實驗平臺

$ lsb_release -a # 查看當前實驗平臺系統發行版的具體版本號 
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 20.04.2 LTS
Release:	20.04
Codename:	focal
  • 請選擇如下合適的方式運行Linux實驗操作系統
    • 物理機/虛擬機(Virtual Box, Vmware…)
    • Windows Subsystem for Linux(WSL)
    • Docker鏡像運行

3.2 實驗所需的工具/源代碼

  • QEMU 6.0.0
  • Linux Kernel 5.10.42(LTS)
  • [RISC‑V Compiler Toolchain](http://riscv-gnu-toolchain: riscv-gnu-toolchain 是一個用來支持 RISC-V 為後端的C和C++交叉編譯工具鏈, 包含通用的ELF/Newlib和更復雜的Linux-ELF/glibc兩種 (gitee.com))

4 背景知識

4.1 RISC-V:自由和開放的RISC指令集架構

RISC-V ISA發端於深厚的學術研究,將免費且可擴展的軟硬件架構自由度提升至新的水平,為未來50年的計算設計與創新鋪平了道路。

4.2 QEMU:一款開源且通用的模擬器和虛擬機

4.2.1 什麼是QEMU

QEMU最開始是由法國程序員Fabrice Bellard開發的模擬器。QEMU能夠完成用戶程序模擬和系統虛擬化模擬。用戶程序模擬指的是QEMU能夠將為一個平臺編譯的二進制文件運行在另一個不同的平臺,如一個ARM指令集的二進製程序,通過QEMU的TCG(Tiny Code Generator)引擎的處理之後,ARM指令被轉化為TCG中間代碼,然後再轉化為目標平臺(比如Intel x86)的代碼。系統虛擬化模擬指的是QEMU能夠模擬一個完整的系統虛擬機,該虛擬機有自己的虛擬CPU,芯片組,虛擬內存以及各種虛擬外部設備,能夠為虛擬機中運行的操作系統和應用軟件呈現出與物理計算機完全一致的硬件視圖。

4.2.2 如何使用 QEMU(常見參數介紹)

以以下命令為例,我們簡單介紹QEMU的參數所代表的含義

$ qemu-system-riscv64 -nographic -machine virt -kernel build/linux/arch/riscv/boot/Image  \
 -device virtio-blk-device,drive=hd0 -append "root=/dev/vda ro console=ttyS0"   \
 -bios default -drive file=rootfs.ext4,format=raw,id=hd0 \
 -netdev user,id=net0 -device virtio-net-device,netdev=net0 -S -s

-nographic: 不使用圖形窗口,使用命令行 -machine: 指定要emulate的機器,可以通過命令qemu-system-riscv64 -machine help查看可選擇的機器選項 -kernel: 指定內核image -append cmdline: 使用cmdline作為內核的命令行 -device: 指定要模擬的設備,可以通過命令qemu-system-riscv64 -device help查看可選擇的設備,通過命令qemu-system-riscv64 -device <具體的設備>,help查看某個設備的命令選項 -drive, file=<file_name>: 使用’file’作為文件系統 -netdev user,id=str: 指定user mode的虛擬網卡, 指定ID為str -S: 啟動時暫停CPU執行(使用’c’啟動執行) -s: -gdb tcp::1234 的簡寫 -bios default: 使用默認的OpenSBI firmware作為bootloader

更多參數信息可以參考這裡

4.3 Linux 內核

Linux is a clone of the operating system Unix, written from scratch by Linus Torvalds with assistance from a loosely-knit team of hackers across the Net. It aims towards POSIX and Single UNIX Specification compliance.

It has all the features you would expect in a modern fully-fledged Unix, including true multitasking, virtual memory, shared libraries, demand loading, shared copy-on-write executables, proper memory management, and multistack networking including IPv4 and IPv6.

Although originally developed first for 32-bit x86-based PCs (386 or higher), today Linux also runs on a multitude of other processor architectures, in both 32- and 64-bit variants.

4.3.1 Linux 使用基礎

在Linux環境下,人們通常使用命令行接口來完成與計算機的交互。終端(Terminal)是用於處理該過程的一個應用程序,通過終端你可以運行各種程序以及在自己的計算機上處理文件。在類Unix的操作系統上,終端可以為你完成一切你所需要的操作。 下面我們僅對實驗中涉及的一些概念進行介紹,你可以通過下面的鏈接來對命令行的使用進行學習:

  1. The Missing Semester of Your CS Education >>Video<<
  2. GNU/Linux Command-Line Tools Summary
  3. Basics of UNIX

4.3.2 Linux 環境變量

當我們在終端輸入命令時,終端會找到對應的程序來運行。我們可以通過which命令來做一些小的實驗:

$ which gcc
/usr/bin/gcc
$ ls -l /usr/bin/gcc
lrwxrwxrwx 1 root root 5 5月  21  2019 /usr/bin/gcc -> gcc-7

可以看到,當我們在輸入gcc命令時,終端實際執行的程序是/usr/bin/gcc。實際上,終端在執行命令時,會從PATH環境變量所包含的地址中查找對應的程序來執行。我們可以將PATH變量打印出來來檢查一下其是否包含/usr/bin

$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/phantom/.local/bin

在後面的實驗中,如果你想直接訪問riscv64-unknown-linux-gnu-gccqemu-system-riscv64等程序,那麼你需要把他們所在的目錄添加到目錄中。

$ export PATH=$PATH:/opt/riscv/bin

4.4 交叉編譯工具鏈

compiler is a kind of computer software that compiles a high-level language (such as C) into machine instructions that can be executed by the target CPU platform.

4.4.1 編譯器的類別

這裡首先介紹如下三種概念名詞:

  • *The build system: The machine which generates the compiler binaries.*
  • *The host system: The system which runs the compiler binaries.*
  • *The target system: The system which runs the application code compiled by the compiler binari*

根據上述不同平臺之間的異同,編譯器可以分為如下幾種類型:

  • native compiler: A compiler where target is the same system as host.
  • cross compiler: A compiler where target is not the same system as host.

其中**交叉編譯工具鏈(Cross Compiler)**是我們在本系列實驗中所採用的主要編譯工具。交叉編譯指的是在一個平臺上編譯可以在另一個平臺運行的程序,例如在x86機器上編譯可以在arm平臺運行的程序,交叉編譯需要交叉編譯工具鏈的支持。

在後續實驗中我們令:

build = X86_64 linux
host =  X86_64 linux
target = RISC-V64 

4.4.2 GNU GCC

GCC stands for GNU Compiler Collection

GCC is an integrated distribution of compilers for several major programming languages. These languages currently include C, C++,Objective-C, Objective-C++, Fortran, Ada, D, Go, and BRIG (HSAIL)

GCC 11.1 Manual

Using the GNU Compiler Collection For gcc version 11.1.0 Richard M. Stallman and the GCC Developer Community

4.4.2 GNU Binutils

The GNU Binutils are a collection of binary tools. The main ones are:

  • ld – the GNU linker.
  • as – the GNU assembler.

But they also include:

  • addr2line – Converts addresses into filenames and line numbers.
  • ar – A utility for creating, modifying and extracting from archives.
  • c++filt – Filter to demangle encoded C++ symbols.
  • dlltool – Creates files for building and using DLLs.
  • gold – A new, faster, ELF only linker, still in beta test.
  • gprof – Displays profiling information.
  • nlmconv – Converts object code into an NLM.
  • nm – Lists symbols from object files.
  • objcopy – Copies and translates object files.
  • objdump – Displays information from object files.
  • ranlib – Generates an index to the contents of an archive.
  • readelf – Displays information from any ELF format object file.
  • size – Lists the section sizes of an object or archive file.
  • strings – Lists printable strings from files.
  • strip – Discards symbols.
  • windmc – A Windows compatible message compiler.
  • windres – A compiler for Windows resource files.

Most of these programs use BFD, the Binary File Descriptor library, to do low-level manipulation. Many of them also use the opcodes library to assemble and disassemble machine instructions.

GNU Binutils

GNU Operating System Supported by the Free Software Foundation

4.5 GDB 使用基礎

4.5.1 什麼是 GDB

GNU調試器(英語:GNU Debugger,縮寫:gdb)是一個由GNU開源組織發佈的、UNIX/LINUX操作系統下的、基於命令行的、功能強大的程序調試工具。藉助調試器,我們能夠查看另一個程序在執行時實際在做什麼(比如訪問哪些內存、寄存器),在其他程序崩潰的時候可以比較快速地瞭解導致程序崩潰的原因。 被調試的程序可以是和gdb在同一臺機器上(本地調試,or native debug),也可以是不同機器上(遠程調試, or remote debug)。

總的來說,gdb可以有以下4個功能

  • 啟動程序,並指定可能影響其行為的所有內容
  • 使程序在指定條件下停止
  • 檢查程序停止時發生了什麼
  • 更改程序中的內容,以便糾正一個bug的影響

4.5.2 GDB 基本命令介紹

(gdb) start:單步執行,運行程序,停在第一執行語句 (gdb) next:單步調試(逐過程,函數直接執行),簡寫n (gdb) run:重新開始運行文件(run-text:加載文本文件,run-bin:加載二進制文件),簡寫r (gdb) backtrace:查看函數的調用的棧幀和層級關係,簡寫bt (gdb) break 設置斷點。比如斷在具體的函數就break func;斷在某一行break filename:num (gdb) finish:結束當前函數,返回到函數調用點 (gdb) frame:切換函數的棧幀,簡寫f (gdb) print:打印值及地址,簡寫p (gdb) info:查看函數內部局部變量的數值,簡寫i;查看寄存器的值i register xxx (gdb) display:追蹤查看具體變量值

更多命令可以參考100個gdb小技巧

4.5.3 GDB 插件使用(不做要求)

單純使用gdb比較繁瑣不是很方便,我們可以使用gdb插件讓調試過程更有效率。推薦各位同學使用gef,由於當前工具鏈還不支持 python3,請使用舊版本的gef-legacy。 該倉庫中已經取消的原有的安裝腳本,同學們可以把 gef.py 腳本拷貝下來,直接在 .gdbinit 中引導,感興趣的同學可以參考這篇文章(內網訪問)

4.6 LINUX 內核編譯基礎

4.6.1 內核配置

內核配置是用於配置是否啟用內核的各項特性,內核會提供一個名為 defconfig(即default configuration) 的默認配置,該配置文件位於各個架構目錄的 configs 文件夾下,例如對於RISC-V而言,其默認配置文件為 arch/riscv/configs/defconfig。使用 make ARCH=riscv defconfig 命令可以在內核根目錄下生成一個名為 .config 的文件,包含了內核完整的配置,內核在編譯時會根據 .config 進行編譯。配置之間存在相互的依賴關係,直接修改defconfig文件或者 .config 有時候並不能達到想要的效果。因此如果需要修改配置一般採用 make ARCH=riscv menuconfig 的方式對內核進行配置。

4.6.2 常見參數

ARCH 指定架構,可選的值包括arch目錄下的文件夾名,如x86,arm,arm64等,不同於arm和arm64,32位和64位的RISC-V共用 arch/riscv 目錄,通過使用不同的config可以編譯32位或64位的內核。

CROSS_COMPILE 指定使用的交叉編譯工具鏈,例如指定 CROSS_COMPILE=aarch64-linux-gnu-,則編譯時會採用 aarch64-linux-gnu-gcc 作為編譯器,編譯可以在arm64平臺上運行的kernel。

CC 指定編譯器,通常指定該變量是為了使用clang編譯而不是用gcc編譯,Linux內核在逐步提供對clang編譯的支持,arm64和x86已經能夠很好的使用clang進行編譯。

4.6.3 常用編譯選項

$ make defconfig	        ### 使用當前平臺的默認配置,在x86機器上會使用x86的默認配置
$ make -j$(nproc)	        ### 編譯當前平臺的內核,-j$(nproc)為以機器硬件線程數進行多線程編譯

$ make ARCH=riscv defconfig	### 使用RISC-V平臺的默認配置
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- -j$(nproc)     ### 編譯RISC-V平臺內核

$ make clean	                ### 清除所有編譯好的object文件
$ make mrproper	                ### 清除編譯的配置文件,中間文件和結果文件

$ make init/main.o	        ### 編譯當前平臺的單個object文件init/main.o(會同時編譯依賴的文件)

5 實驗步驟

通常情況下,$ 提示符表示當前運行的用戶為普通用戶,# 代表當前運行的用戶為特權用戶。 但注意,在下文的示例中,以 ### 開頭的行代表註釋,$ 開頭的行代表在你的宿主機/虛擬機上運行的命令,# 開頭的行代表在 docker 中運行的命令,(gdb) 開頭的行代表在 gdb 中運行的命令。 在執行每一條命令前,請你對將要進行的操作進行思考,給出的命令不需要全部執行,並且不是所有的命令都可以無條件執行,請不要直接複製粘貼命令去執行。

5.1 實驗環境設置

5.1.1 下載安裝必要的工具和庫

$ sudo apt update
$ sudo apt upgrade
$ sudo apt install git \
        autoconf \
        automake \
        autotools-dev \
        ninja-build \
        build-essential \
        libmpc-dev \
        libmpfr-dev \
        libgmp-dev \
        libglib2.0-dev \
        libpixman-1-dev \
        libncurses5-dev \
        libtool \
        libexpat-dev \
        zlib1g-dev \
        curl \
        gawk \
        bison \
        flex \
        texinfo \
        gperf \
        patchutils \
        bc 

5.1.2 下載和安裝必要的工具鏈以及源代碼包

5.1.2.1 編譯安裝 QEMU 5.0.0
$ mkdir riscv_oslab
$ cd ~/riscv64_oslab  ### 進入實驗工作目錄,然後在線獲取QEMU 5.0.0版本的源代碼安裝包到當前目錄下
$ wget https://download.qemu.org/qemu-5.0.0.tar.xz 
$ tar -xvJf qemu-5.0.0.tar.xz ### 解壓縮源代碼包到qemu-5.0.0文件夾中
$ cd qemu-5.0.0 ### 進入該目錄
$ ./configure --static --target-list=riscv64-softmmu,riscv64-linux-user ### 編譯前配置好QEMU,病設置目標處理器架構為"64位的RISC-V"
$ make -j16 ### 編譯,可用nproc查看可用的線程數
$ make install ### 安裝QEMU到默認位置

此處編譯qemu的時候,目標選擇了 riscv64-softmmu 和 riscv64-linux-user,兩者具體的區別為:

To put it simply, xxx-softmmu will compile qemu-system-xxx, which is an emulated machine for xxx architecture (System Emulation). When it resets, the starting point will be the reset vector of that architecture. While xxx-linux-user, compiles qemu-xxx, which allows you to run user application in xxx architecture (User-mode Emulation). Which will seek the user applications’ main function, and start execution from there.

安裝完畢後如果執行如下命令後能夠查看到qemu的具體版本,則說明安裝成功

$ qemu-system-riscv64 --version
QEMU emulator version 5.0.0
Copyright (c) 2003-2021 Fabrice Bellard and the QEMU Project developers
5.1.2.2 下載或編譯安裝 RISC‑V GCC Toolchain

途徑1(推薦):下載預編譯版本的 riscv-gnu-toolchain

  • 下載newlibc版本的riscv-gnu-toolchain,地址:https://www.sifive.com/software

這裡可選擇下載:GNU Embedded Toolchain — v2020.12.8 Ubuntu版本的 Prebuilt RISC‑V GCC Toolchain(帶有gdb調試工具)在此您也可以選擇運行在其它系統環境下的riscv-gnu-toolchain.

$ cd ~/riscv64_oslab
$ wget https://static.dev.sifive.com/dev-tools/freedom-tools/v2020.12/riscv64-unknown-elf-toolchain-10.2.0-2020.12.8-x86_64-linux-ubuntu14.tar.gz
$ tar -xzvf riscv64-unknown-elf-toolchain-10.2.0-2020.12.8-x86_64-linux-ubuntu14.tar.gz ###解壓
$ mv riscv64-unknown-elf-toolchain-10.2.0-2020.12.8-x86_64-linux-ubuntu14 \
     riscv64-unknown-elf-toolchain ### 重命名文件夾
$ 編輯~/.bashrc 在文件末尾添加如下環境變量設置語句
export PATH=~/riscv_oslab/riscv64-unknown-elf-toolchain/bin:$PATH
$ source ~/.bashrc
$ riscv64-unknown-elf-gcc --version ### 查看gcc版本
riscv64-unknown-elf-gcc (SiFive GCC-Metal 10.2.0-2020.12.8) 10.2.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  • 安裝 glibc 版本的 riscv-gnu-toolchain
$ sudo apt install binutils-riscv64-linux-gnu
$ sudo apt install gcc-riscv64-linux-gnu
$ riscv64-linux-gnu-gcc --version ### 查看gcc版本
riscv64-linux-gnu-gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

途徑2:從源代碼編譯獲得

首先下載 riscv-gnu-toolchain倉庫

$ git clone --recursive https://github.com/riscv/riscv-gnu-toolchain

注意:採用上述Git clone的方式來獲得riscv-gnu-toolchain可能需要6.65GB的空間

編譯安裝步驟:

./configure --prefix=/opt/riscv
make
5.1.2.3 使用上述交叉編譯工具鏈編譯Linux 5.10.42版本的內核
$ cd ~/riscv64_oslab
$ wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.10.42.tar.gz
$ tar -xzvf linux-5.10.42.tar.gz
$ cd linux-5.10.42
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- defconfig
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- -j $(nproc)
  ...
  GEN     .version
  CHK     include/generated/compile.h
  LD      vmlinux.o
  MODPOST vmlinux.symvers
  MODINFO modules.builtin.modinfo
  GEN     modules.builtin
  LD      .tmp_vmlinux.kallsyms1
  KSYMS   .tmp_vmlinux.kallsyms1.S
  AS      .tmp_vmlinux.kallsyms1.S
  LD      .tmp_vmlinux.kallsyms2
  KSYMS   .tmp_vmlinux.kallsyms2.S
  AS      .tmp_vmlinux.kallsyms2.S
  LD      vmlinux
  SYSMAP  System.map
  MODPOST modules-only.symvers
  GEN     Module.symvers
  OBJCOPY arch/riscv/boot/Image
  CC [M]  fs/efivarfs/efivarfs.mod.o
  GZIP    arch/riscv/boot/Image.gz
  LD [M]  fs/efivarfs/efivarfs.ko
  Kernel: arch/riscv/boot/Image.gz is ready
5.1.2.4 使用上述交叉編譯工具鏈編譯Busybox
$ cd ~/riscv64_oslab
$ wget https://busybox.net/downloads/busybox-1.33.1.tar.bz2
$ tar  -jxvf  busybox-1.33.1.tar.bz2 
$ cd busybox-1.33.1
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- defconfig
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- menuconfig ### 這裡打開後可以選擇直接exit,並保存到配置文件.config中
$ 打開.config文件並編輯添加 
  CONFIG_STATIC=y
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- -j $(nproc)
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- install
$ cd _install
$ mkdir proc sys dev etc etc/init.d
$ ls ### 查看目錄結構
bin  dev  etc  linuxrc  proc  sbin  sys  usr
$ touch etc/init.d/rcS 並編輯文件內容為如下: ### We will now create a bash script in order to mount some devices automatically after the boot.
$ sudo mknod dev/console c 5 1
$ sudo mknod dev/ram b 1 0
#!bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
/sbin/mdev -s
$ chmod +x etc/init.d/rcS # Now change the file’s mode as executable.
$ find -print0 | cpio -0oH newc | gzip -9 > ../rootfs.img
$ cd ..
5.1.2.5 使用qemu 運行我們的linux kernel

從上述的實驗步驟中我們分別獲得了

~/riscv_oslab/busybox-1.33.1/rootfs.img
~/riscv_oslab/linux-5.10.42/arch/riscv/boot/Image

下面我們在QEMU中模擬運行我們地Linux內核:

執行如下命令行:

$ qemu-system-riscv64 \
        -nographic -machine virt \
        -kernel ~/riscv_oslab/linux-5.10.42/arch/riscv/boot/Image \
        -initrd ~/riscv_oslab/busybox-1.33.1/rootfs.img  \
        -append "root=/dev/ram rdinit=/sbin/init"

可以看到整個Linux Kernel地啟動流程:

OpenSBI v0.9
   ____                    _____ ____ _____
  / __ \                  / ____|  _ \_   _|
 | |  | |_ __   ___ _ __ | (___ | |_) || |
 | |  | | '_ \ / _ \ '_ \ \___ \|  _ < | |
 | |__| | |_) |  __/ | | |____) | |_) || |_
  \____/| .__/ \___|_| |_|_____/|____/_____|
        | |
        |_|

Platform Name             : riscv-virtio,qemu
Platform Features         : timer,mfdeleg
Platform HART Count       : 1
Firmware Base             : 0x80000000
Firmware Size             : 100 KB
Runtime SBI Version       : 0.2

Domain0 Name              : root
Domain0 Boot HART         : 0
Domain0 HARTs             : 0*
Domain0 Region00          : 0x0000000080000000-0x000000008001ffff ()
Domain0 Region01          : 0x0000000000000000-0xffffffffffffffff (R,W,X)
Domain0 Next Address      : 0x0000000080200000
Domain0 Next Arg1         : 0x0000000087000000
Domain0 Next Mode         : S-mode
Domain0 SysReset          : yes

Boot HART ID              : 0
Boot HART Domain          : root
Boot HART ISA             : rv64imafdcsu
Boot HART Features        : scounteren,mcounteren,time
Boot HART PMP Count       : 16
Boot HART PMP Granularity : 4
Boot HART PMP Address Bits: 54
Boot HART MHPM Count      : 0
Boot HART MHPM Count      : 0
Boot HART MIDELEG         : 0x0000000000000222
Boot HART MEDELEG         : 0x000000000000b109
[    0.000000] Linux version 5.10.42 (nn@ubuntu) (riscv64-linux-gnu-gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1 SMP Sat Jun 26 22:53:26 PDT 2021
[    0.000000] OF: fdt: Ignoring memory range 0x80000000 - 0x80200000
[    0.000000] efi: UEFI not found.
[    0.000000] Initial ramdisk at: 0x(____ptrval____) (1146880 bytes)
[    0.000000] Zone ranges:
[    0.000000]   DMA32    [mem 0x0000000080200000-0x0000000087ffffff]
[    0.000000]   Normal   empty
[    0.000000] Movable zone start for each node
[    0.000000] Early memory node ranges
[    0.000000]   node   0: [mem 0x0000000080200000-0x0000000087ffffff]
[    0.000000] Initmem setup node 0 [mem 0x0000000080200000-0x0000000087ffffff]
[    0.000000] software IO TLB: Cannot allocate buffer
[    0.000000] SBI specification v0.2 detected
[    0.000000] SBI implementation ID=0x1 Version=0x9
[    0.000000] SBI v0.2 TIME extension detected
[    0.000000] SBI v0.2 IPI extension detected
[    0.000000] SBI v0.2 RFENCE extension detected
[    0.000000] SBI v0.2 HSM extension detected
[    0.000000] riscv: ISA extensions acdfimsu
[    0.000000] riscv: ELF capabilities acdfim
[    0.000000] percpu: Embedded 17 pages/cpu s32360 r8192 d29080 u69632
[    0.000000] Built 1 zonelists, mobility grouping on.  Total pages: 31815
[    0.000000] Kernel command line: root=/dev/ram rdinit=/sbin/init
[    0.000000] Dentry cache hash table entries: 16384 (order: 5, 131072 bytes, linear)
[    0.000000] Inode-cache hash table entries: 8192 (order: 4, 65536 bytes, linear)
[    0.000000] Sorting __ex_table...
[    0.000000] mem auto-init: stack:off, heap alloc:off, heap free:off
[    0.000000] Memory: 108180K/129024K available (6954K kernel code, 4125K rwdata, 4096K rodata, 223K init, 342K bss, 20844K reserved, 0K cma-reserved)
[    0.000000] Virtual kernel memory layout:
[    0.000000]       fixmap : 0xffffffcefee00000 - 0xffffffceff000000   (2048 kB)
[    0.000000]       pci io : 0xffffffceff000000 - 0xffffffcf00000000   (  16 MB)
[    0.000000]      vmemmap : 0xffffffcf00000000 - 0xffffffcfffffffff   (4095 MB)
[    0.000000]      vmalloc : 0xffffffd000000000 - 0xffffffdfffffffff   (65535 MB)
[    0.000000]       lowmem : 0xffffffe000000000 - 0xffffffe007e00000   ( 126 MB)
[    0.000000] SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=1, Nodes=1
[    0.000000] rcu: Hierarchical RCU implementation.
[    0.000000] rcu: 	RCU restricting CPUs from NR_CPUS=8 to nr_cpu_ids=1.
[    0.000000] rcu: 	RCU debug extended QS entry/exit.
[    0.000000] 	Tracing variant of Tasks RCU enabled.
[    0.000000] rcu: RCU calculated value of scheduler-enlistment delay is 25 jiffies.
[    0.000000] rcu: Adjusting geometry for rcu_fanout_leaf=16, nr_cpu_ids=1
[    0.000000] NR_IRQS: 64, nr_irqs: 64, preallocated irqs: 0
[    0.000000] riscv-intc: 64 local interrupts mapped
[    0.000000] plic: plic@c000000: mapped 53 interrupts with 1 handlers for 2 contexts.
[    0.000000] random: get_random_bytes called from start_kernel+0x312/0x484 with crng_init=0
[    0.000000] riscv_timer_init_dt: Registering clocksource cpuid [0] hartid [0]
[    0.000000] clocksource: riscv_clocksource: mask: 0xffffffffffffffff max_cycles: 0x24e6a1710, max_idle_ns: 440795202120 ns
[    0.000155] sched_clock: 64 bits at 10MHz, resolution 100ns, wraps every 4398046511100ns
[    0.003256] Console: colour dummy device 80x25
[    0.004684] printk: console [tty0] enabled
[    0.008705] Calibrating delay loop (skipped), value calculated using timer frequency.. 20.00 BogoMIPS (lpj=40000)
[    0.008862] pid_max: default: 32768 minimum: 301
[    0.009925] Mount-cache hash table entries: 512 (order: 0, 4096 bytes, linear)
[    0.009977] Mountpoint-cache hash table entries: 512 (order: 0, 4096 bytes, linear)
[    0.031354] rcu: Hierarchical SRCU implementation.
[    0.033066] EFI services will not be available.
[    0.034680] smp: Bringing up secondary CPUs ...
[    0.034774] smp: Brought up 1 node, 1 CPU
[    0.042458] devtmpfs: initialized
[    0.047904] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645041785100000 ns
[    0.048157] futex hash table entries: 256 (order: 2, 16384 bytes, linear)
[    0.052753] NET: Registered protocol family 16
[    0.096960] vgaarb: loaded
[    0.097897] SCSI subsystem initialized
[    0.099470] usbcore: registered new interface driver usbfs
[    0.099708] usbcore: registered new interface driver hub
[    0.099841] usbcore: registered new device driver usb
[    0.109735] clocksource: Switched to clocksource riscv_clocksource
[    0.121388] NET: Registered protocol family 2
[    0.122636] IP idents hash table entries: 2048 (order: 2, 16384 bytes, linear)
[    0.125264] tcp_listen_portaddr_hash hash table entries: 128 (order: 0, 5120 bytes, linear)
[    0.125384] TCP established hash table entries: 1024 (order: 1, 8192 bytes, linear)
[    0.125566] TCP bind hash table entries: 1024 (order: 3, 32768 bytes, linear)
[    0.125704] TCP: Hash tables configured (established 1024 bind 1024)
[    0.126589] UDP hash table entries: 256 (order: 2, 24576 bytes, linear)
[    0.126838] UDP-Lite hash table entries: 256 (order: 2, 24576 bytes, linear)
[    0.127964] NET: Registered protocol family 1
[    0.130578] RPC: Registered named UNIX socket transport module.
[    0.130640] RPC: Registered udp transport module.
[    0.130666] RPC: Registered tcp transport module.
[    0.130690] RPC: Registered tcp NFSv4.1 backchannel transport module.
[    0.130796] PCI: CLS 0 bytes, default 64
[    0.132983] Unpacking initramfs...
[    0.184451] Freeing initrd memory: 1116K
[    0.186811] workingset: timestamp_bits=62 max_order=15 bucket_order=0
[    0.196540] NFS: Registering the id_resolver key type
[    0.197258] Key type id_resolver registered
[    0.197309] Key type id_legacy registered
[    0.197590] nfs4filelayout_init: NFSv4 File Layout Driver Registering...
[    0.197681] nfs4flexfilelayout_init: NFSv4 Flexfile Layout Driver Registering...
[    0.198313] 9p: Installing v9fs 9p2000 file system support
[    0.199295] NET: Registered protocol family 38
[    0.199533] Block layer SCSI generic (bsg) driver version 0.4 loaded (major 251)
[    0.199661] io scheduler mq-deadline registered
[    0.199745] io scheduler kyber registered
[    0.206411] pci-host-generic 30000000.pci: host bridge /soc/pci@30000000 ranges:
[    0.207040] pci-host-generic 30000000.pci:       IO 0x0003000000..0x000300ffff -> 0x0000000000
[    0.207407] pci-host-generic 30000000.pci:      MEM 0x0040000000..0x007fffffff -> 0x0040000000
[    0.207473] pci-host-generic 30000000.pci:      MEM 0x0400000000..0x07ffffffff -> 0x0400000000
[    0.209120] pci-host-generic 30000000.pci: ECAM at [mem 0x30000000-0x3fffffff] for [bus 00-ff]
[    0.209814] pci-host-generic 30000000.pci: PCI host bridge to bus 0000:00
[    0.210081] pci_bus 0000:00: root bus resource [bus 00-ff]
[    0.210175] pci_bus 0000:00: root bus resource [io  0x0000-0xffff]
[    0.210203] pci_bus 0000:00: root bus resource [mem 0x40000000-0x7fffffff]
[    0.210228] pci_bus 0000:00: root bus resource [mem 0x400000000-0x7ffffffff]
[    0.211207] pci 0000:00:00.0: [1b36:0008] type 00 class 0x060000
[    0.257315] Serial: 8250/16550 driver, 4 ports, IRQ sharing disabled
[    0.263577] 10000000.uart: ttyS0 at MMIO 0x10000000 (irq = 2, base_baud = 230400) is a 16550A
[    0.293271] printk: console [ttyS0] enabled
[    0.298044] [drm] radeon kernel modesetting enabled.
[    0.311076] loop: module loaded
[    0.313675] libphy: Fixed MDIO Bus: probed
[    0.314711] e1000e: Intel(R) PRO/1000 Network Driver
[    0.314950] e1000e: Copyright(c) 1999 - 2015 Intel Corporation.
[    0.315481] ehci_hcd: USB 2.0 'Enhanced' Host Controller (EHCI) Driver
[    0.315834] ehci-pci: EHCI PCI platform driver
[    0.316191] ehci-platform: EHCI generic platform driver
[    0.316510] ohci_hcd: USB 1.1 'Open' Host Controller (OHCI) Driver
[    0.316869] ohci-pci: OHCI PCI platform driver
[    0.317290] ohci-platform: OHCI generic platform driver
[    0.318730] usbcore: registered new interface driver uas
[    0.319250] usbcore: registered new interface driver usb-storage
[    0.320182] mousedev: PS/2 mouse device common for all mice
[    0.323094] goldfish_rtc 101000.rtc: registered as rtc0
[    0.323986] goldfish_rtc 101000.rtc: setting system clock to 2021-06-27T06:45:02 UTC (1624776302)
[    0.326296] syscon-poweroff soc:poweroff: pm_power_off already claimed (____ptrval____) sbi_shutdown
[    0.326805] syscon-poweroff: probe of soc:poweroff failed with error -16
[    0.327990] usbcore: registered new interface driver usbhid
[    0.328373] usbhid: USB HID core driver
[    0.330278] NET: Registered protocol family 10
[    0.337351] Segment Routing with IPv6
[    0.338095] sit: IPv6, IPv4 and MPLS over IPv4 tunneling driver
[    0.340452] NET: Registered protocol family 17
[    0.341736] 9pnet: Installing 9P2000 support
[    0.342188] Key type dns_resolver registered
[    0.342886] debug_vm_pgtable: [debug_vm_pgtable         ]: Validating architecture page table helpers
[    0.378742] Freeing unused kernel memory: 220K
[    0.381020] Run /sbin/init as init process

Please press Enter to activate this console. 

由於以安裝了Busy Box並製作了rootfs,因此可以運行較多的常用命令,示例如下:

/ # ls
bin      etc      proc     sbin     usr
dev      linuxrc  root     sys
/ # pwd
/ # cd bin
/bin # ls
arch           dumpkmap       kill           netstat        setarch
ash            echo           link           nice           setpriv
base32         ed             linux32        nuke           setserial
base64         egrep          linux64        pidof          sh
busybox        false          ln             ping           sleep
cat            fatattr        login          ping6          stat
chattr         fdflush        ls             pipe_progress  stty
chgrp          fgrep          lsattr         printenv       su
chmod          fsync          lzop           ps             sync
chown          getopt         makemime       pwd            tar
conspy         grep           mkdir          reformime      touch
cp             gunzip         mknod          resume         true
cpio           gzip           mktemp         rev            umount
cttyhack       hostname       more           rm             uname
date           hush           mount          rmdir          usleep
dd             ionice         mountpoint     rpm            vi
df             iostat         mpstat         run-parts      watch
dmesg          ipcalc         mt             scriptreplay   zcat
dnsdomainname  kbd_mode       mv             sed
/bin # 
退出QEMU模擬器的方法為:
使用ctrl+a(macOS下為control+a),鬆開後再按下x鍵即可退出qemu

Debug with GDB

接下來使用GDB來調試運行在QEMU中地Linux Kernel:

在之前運行QEMU啟動Linux Kernel的命令行之後追加如下兩個參數 -s -S

$ qemu-system-riscv64 \
        -nographic -machine virt \
        -kernel ~/riscv_oslab/linux-5.10.42/arch/riscv/boot/Image \
        -initrd ~/riscv_oslab/busybox-1.33.1/rootfs.img  \
        -append "root=/dev/ram rdinit=/sbin/init" \
        -s -S

然後打開另一個終端,使用 riscv64-unknown-elf-gdb 進行內核的調試工作:

查看gdb的版本號:

$ riscv64-unknown-elf-gdb --version
GNU gdb (SiFive GDB-Metal 10.1.0-2020.12.7) 10.1
$ riscv64-unknown-elf-gdb ~/riscv_oslab/linux-5.10.42/vmlinux

若gdb提示如下信息:

Reading symbols from .../vmlinux…
(No debugging symbols found in .../vmlinux)

需要重新編譯內核,需要在內核Makefile的KBUILD_CFLAGS上添加**-g**選項,然後繼續運行上述命令行啟動gdb開始調試

在gdb中添加斷點,如在start_kernel處添加斷點後,輸⼊continue,則qemu會在初始化kernel的過程中停止,如下所示:

(gdb) remote target localhost:1234 ### 連接本地Qemu調試端口
(gdb) b start_kernel 
Breakpoint 1 at 0xffffffe00000272e
(gdb) continue
Continuing.
Breakpoint 1, 0xffffffe00000272e in start_kernel ()

使用GDB+QEMU偵錯64位RISC-V LINUX 核心

https://www.nuanyun.cloud/?p=1481

1 實驗目標

使用GDB在QEMU模擬器中偵錯運行基於64位RISCV架構的Linux核心。

2 實驗簡介

本次實驗將幫助我們更好地瞭解如何使用交叉編譯工具鏈編譯Linux核心原始碼,並將編譯所得到的基於RISCV指令集架構的Linux核心運行在QEMU模擬器上。本次實驗還將幫助我們更好地瞭解掌握如何利用GDB和QEMU聯合偵錯核心運行。

關鍵詞: Qemu, Kernel, Linux, OS, RISC-V, Cross Compiler Toolchain, GDB

3 實驗環境

3.1 實驗平臺

$ lsb_release -a # 查看當前實驗平臺系統發行版的具體版本號 
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 20.04.2 LTS
Release:	20.04
Codename:	focal
  • 請選擇如下合適的方式運行Linux實驗作業系統
    • 物理機/虛擬機器(Virtual Box, Vmware…)
    • Windows Subsystem for Linux(WSL)
    • Docker鏡像運行

3.2 實驗所需的工具/原始碼

  • QEMU 6.0.0
  • Linux Kernel 5.10.42(LTS)
  • [RISC‑V Compiler Toolchain](http://riscv-gnu-toolchain: riscv-gnu-toolchain 是一個用來支持 RISC-V 為後端的C和C++交叉編譯工具鏈, 包含通用的ELF/Newlib和更復雜的Linux-ELF/glibc兩種 (gitee.com))

4 背景知識

4.1 RISC-V:自由和開放的RISC指令集架構

RISC-V ISA發端於深厚的學術研究,將免費且可擴展的軟硬體架構自由度提升至新的水平,為未來50年的計算設計與創新鋪平了道路。

4.2 QEMU:一款開源且通用的模擬器和虛擬機器

4.2.1 什麼是QEMU

QEMU最開始是由法國程式設計師Fabrice Bellard開發的模擬器。QEMU能夠完成使用者程序模擬和系統虛擬化模擬。使用者程序模擬指的是QEMU能夠將為一個平臺編譯的二進制檔案運行在另一個不同的平臺,如一個ARM指令集的二進製程序,通過QEMU的TCG(Tiny Code Generator)引擎的處理之後,ARM指令被轉化為TCG中間程式碼,然後再轉化為目標平臺(比如Intel x86)的程式碼。系統虛擬化模擬指的是QEMU能夠模擬一個完整的系統虛擬機器,該虛擬機器有自己的虛擬CPU,晶片組,虛擬記憶體以及各種虛擬外部裝置,能夠為虛擬機器中運行的作業系統和應用軟體呈現出與物理電腦完全一致的硬體檢視。

4.2.2 如何使用 QEMU(常見參數介紹)

以以下命令為例,我們簡單介紹QEMU的參數所代表的含義

$ qemu-system-riscv64 -nographic -machine virt -kernel build/linux/arch/riscv/boot/Image  \
 -device virtio-blk-device,drive=hd0 -append "root=/dev/vda ro console=ttyS0"   \
 -bios default -drive file=rootfs.ext4,format=raw,id=hd0 \
 -netdev user,id=net0 -device virtio-net-device,netdev=net0 -S -s

-nographic: 不使用圖形窗口,使用命令列 -machine: 指定要emulate的機器,可以通過命令qemu-system-riscv64 -machine help查看可選擇的機器選項 -kernel: 指定核心image -append cmdline: 使用cmdline作為核心的命令列 -device: 指定要模擬的裝置,可以通過命令qemu-system-riscv64 -device help查看可選擇的裝置,通過命令qemu-system-riscv64 -device <具體的裝置>,help查看某個裝置的命令選項 -drive, file=<file_name>: 使用’file’作為檔案系統 -netdev user,id=str: 指定user mode的虛擬網路卡, 指定ID為str -S: 啟動時暫停CPU執行(使用’c’啟動執行) -s: -gdb tcp::1234 的簡寫 -bios default: 使用默認的OpenSBI firmware作為bootloader

更多參數資訊可以參考這裡

4.3 Linux 核心

Linux is a clone of the operating system Unix, written from scratch by Linus Torvalds with assistance from a loosely-knit team of hackers across the Net. It aims towards POSIX and Single UNIX Specification compliance.

It has all the features you would expect in a modern fully-fledged Unix, including true multitasking, virtual memory, shared libraries, demand loading, shared copy-on-write executables, proper memory management, and multistack networking including IPv4 and IPv6.

Although originally developed first for 32-bit x86-based PCs (386 or higher), today Linux also runs on a multitude of other processor architectures, in both 32- and 64-bit variants.

4.3.1 Linux 使用基礎

在Linux環境下,人們通常使用命令列介面來完成與電腦的互動。終端(Terminal)是用於處理該過程的一個應用程式,通過終端你可以運行各種程序以及在自己的電腦上處理檔案。在類Unix的作業系統上,終端可以為你完成一切你所需要的操作。 下面我們僅對實驗中涉及的一些概念進行介紹,你可以通過下面的連結來對命令列的使用進行學習:

  1. The Missing Semester of Your CS Education >>Video<<
  2. GNU/Linux Command-Line Tools Summary
  3. Basics of UNIX

4.3.2 Linux 環境變數

當我們在終端輸入命令時,終端會找到對應的程序來運行。我們可以通過which命令來做一些小的實驗:

$ which gcc
/usr/bin/gcc
$ ls -l /usr/bin/gcc
lrwxrwxrwx 1 root root 5 5月  21  2019 /usr/bin/gcc -> gcc-7

可以看到,當我們在輸入gcc命令時,終端實際執行的程序是/usr/bin/gcc。實際上,終端在執行命令時,會從PATH環境變數所包含的地址中尋找對應的程序來執行。我們可以將PATH變數列印出來來檢查一下其是否包含/usr/bin

$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/phantom/.local/bin

在後面的實驗中,如果你想直接訪問riscv64-unknown-linux-gnu-gccqemu-system-riscv64等程序,那麼你需要把他們所在的目錄新增到目錄中。

$ export PATH=$PATH:/opt/riscv/bin

4.4 交叉編譯工具鏈

compiler is a kind of computer software that compiles a high-level language (such as C) into machine instructions that can be executed by the target CPU platform.

4.4.1 編譯器的類別

這裡首先介紹如下三種概念名詞:

  • *The build system: The machine which generates the compiler binaries.*
  • *The host system: The system which runs the compiler binaries.*
  • *The target system: The system which runs the application code compiled by the compiler binari*

根據上述不同平臺之間的異同,編譯器可以分為如下幾種類型:

  • native compiler: A compiler where target is the same system as host.
  • cross compiler: A compiler where target is not the same system as host.

其中**交叉編譯工具鏈(Cross Compiler)**是我們在本系列實驗中改採用的主要編譯工具。交叉編譯指的是在一個平臺上編譯可以在另一個平臺運行的程序,例如在x86機器上編譯可以在arm平臺運行的程序,交叉編譯需要交叉編譯工具鏈的支援。

在後續實驗中我們令:

build = X86_64 linux
host =  X86_64 linux
target = RISC-V64 

4.4.2 GNU GCC

GCC stands for GNU Compiler Collection

GCC is an integrated distribution of compilers for several major programming languages. These languages currently include C, C++,Objective-C, Objective-C++, Fortran, Ada, D, Go, and BRIG (HSAIL)

GCC 11.1 Manual

Using the GNU Compiler Collection For gcc version 11.1.0 Richard M. Stallman and the GCC Developer Community

4.4.2 GNU Binutils

The GNU Binutils are a collection of binary tools. The main ones are:

  • ld – the GNU linker.
  • as – the GNU assembler.

But they also include:

  • addr2line – Converts addresses into filenames and line numbers.
  • ar – A utility for creating, modifying and extracting from archives.
  • c++filt – Filter to demangle encoded C++ symbols.
  • dlltool – Creates files for building and using DLLs.
  • gold – A new, faster, ELF only linker, still in beta test.
  • gprof – Displays profiling information.
  • nlmconv – Converts object code into an NLM.
  • nm – Lists symbols from object files.
  • objcopy – Copies and translates object files.
  • objdump – Displays information from object files.
  • ranlib – Generates an index to the contents of an archive.
  • readelf – Displays information from any ELF format object file.
  • size – Lists the section sizes of an object or archive file.
  • strings – Lists printable strings from files.
  • strip – Discards symbols.
  • windmc – A Windows compatible message compiler.
  • windres – A compiler for Windows resource files.

Most of these programs use BFD, the Binary File Descriptor library, to do low-level manipulation. Many of them also use the opcodes library to assemble and disassemble machine instructions.

GNU Binutils

GNU Operating System Supported by the Free Software Foundation

4.5 GDB 使用基礎

4.5.1 什麼是 GDB

GNU偵錯程式(英語:GNU Debugger,縮寫:gdb)是一個由GNU開源組織發佈的、UNIX/LINUX作業系統下的、基於命令列的、功能強大的程序偵錯工具。藉助偵錯程式,我們能夠查看另一個程序在執行時實際在做什麼(比如訪問哪些記憶體、暫存器),在其他程式當掉的時候可以比較快速地瞭解導致程式當掉的原因。 被偵錯的程序可以是和gdb在同一臺機器上(本地偵錯,or native debug),也可以是不同機器上(遠端偵錯, or remote debug)。

總的來說,gdb可以有以下4個功能

  • 啟動程序,並指定可能影響其行為的所有內容
  • 使程序在指定條件下停止
  • 檢查程序停止時發生了什麼
  • 更改程序中的內容,以便糾正一個bug的影響

4.5.2 GDB 基本命令介紹

(gdb) start:單步執行,運行程序,停在第一執行語句 (gdb) next:單步偵錯(逐過程,函數直接執行),簡寫n (gdb) run:重新開始運行檔案(run-text:載入文字檔,run-bin:載入二進制檔案),簡寫r (gdb) backtrace:查看函數的呼叫的棧幀和層級關係,簡寫bt (gdb) break 設定斷點。比如斷在具體的函數就break func;斷在某一行break filename:num (gdb) finish:結束當前函數,返回到函數呼叫點 (gdb) frame:切換函數的棧幀,簡寫f (gdb) print:列印值及地址,簡寫p (gdb) info:查看函數內部局部變數的數值,簡寫i;查看暫存器的值i register xxx (gdb) display:追蹤查看具體變數值

更多命令可以參考100個gdb小技巧

4.5.3 GDB 外掛使用(不做要求)

單純使用gdb比較繁瑣不是很方便,我們可以使用gdb外掛讓偵錯過程更有效率。推薦各位同學使用gef,由於當前工具鏈還不支援 python3,請使用舊版本的gef-legacy。 該倉庫中已經取消的原有的安裝指令碼,同學們可以把 gef.py 指令碼複製下來,直接在 .gdbinit 中引導,感興趣的同學可以參考這篇文章(內部網路訪問)

4.6 LINUX 核心編譯基礎

4.6.1 核心組態

核心組態是用於組態是否啟用核心的各項特性,核心會提供一個名為 defconfig(即default configuration) 的默認組態,該組態檔案位於各個架構目錄的 configs 資料夾下,例如對於RISC-V而言,其默認組態檔案為 arch/riscv/configs/defconfig。使用 make ARCH=riscv defconfig 命令可以在核心根目錄下生成一個名為 .config 的檔案,包含了核心完整的組態,核心在編譯時會根據 .config 進行編譯。組態之間存在相互的依賴關係,直接修改defconfig檔案或者 .config 有時候並不能達到想要的效果。因此如果需要修改組態一般採用 make ARCH=riscv menuconfig 的方式對核心進行組態。

4.6.2 常見參數

ARCH 指定架構,可選的值包括arch目錄下的資料夾名,如x86,arm,arm64等,不同於arm和arm64,32位和64位的RISC-V共用 arch/riscv 目錄,通過使用不同的config可以編譯32位或64位的核心。

CROSS_COMPILE 指定使用的交叉編譯工具鏈,例如指定 CROSS_COMPILE=aarch64-linux-gnu-,則編譯時會採用 aarch64-linux-gnu-gcc 作為編譯器,編譯可以在arm64平臺上運行的kernel。

CC 指定編譯器,通常指定該變數是為了使用clang編譯而不是用gcc編譯,Linux核心在逐步提供對clang編譯的支援,arm64和x86已經能夠很好的使用clang進行編譯。

4.6.3 常用編譯選項

$ make defconfig	        ### 使用當前平臺的默認組態,在x86機器上會使用x86的默認組態
$ make -j$(nproc)	        ### 編譯當前平臺的核心,-j$(nproc)為以機器硬體執行緒數進行多執行緒編譯

$ make ARCH=riscv defconfig	### 使用RISC-V平臺的默認組態
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- -j$(nproc)     ### 編譯RISC-V平臺核心

$ make clean	                ### 清除所有編譯好的object檔案
$ make mrproper	                ### 清除編譯的組態檔案,中間檔案和結果檔案

$ make init/main.o	        ### 編譯當前平臺的單個object檔案init/main.o(會同時編譯依賴的檔案)

5 實驗步驟

通常情況下,$ 提示符表示當前運行的使用者為普通使用者,# 代表當前運行的使用者為特權使用者。 但注意,在下文的示例中,以 ### 開頭的行代表註釋,$ 開頭的行代表在你的宿主機/虛擬機器上運行的命令,# 開頭的行代表在 docker 中運行的命令,(gdb) 開頭的行代表在 gdb 中運行的命令。 在執行每一條命令前,請你對將要進行的操作進行思考,給出的命令不需要全部執行,並且不是所有的命令都可以無條件執行,請不要直接複製貼上命令去執行。

5.1 實驗環境設定

5.1.1 下載安裝必要的工具和庫

$ sudo apt update
$ sudo apt upgrade
$ sudo apt install git \
        autoconf \
        automake \
        autotools-dev \
        ninja-build \
        build-essential \
        libmpc-dev \
        libmpfr-dev \
        libgmp-dev \
        libglib2.0-dev \
        libpixman-1-dev \
        libncurses5-dev \
        libtool \
        libexpat-dev \
        zlib1g-dev \
        curl \
        gawk \
        bison \
        flex \
        texinfo \
        gperf \
        patchutils \
        bc 

5.1.2 下載和安裝必要的工具鏈以及原始碼包

5.1.2.1 編譯安裝 QEMU 5.0.0
$ mkdir riscv_oslab
$ cd ~/riscv64_oslab  ### 進入實驗工作目錄,然後線上獲取QEMU 5.0.0版本的原始碼安裝包到當前目錄下
$ wget https://download.qemu.org/qemu-5.0.0.tar.xz 
$ tar -xvJf qemu-5.0.0.tar.xz ### 解壓縮原始碼包到qemu-5.0.0資料夾中
$ cd qemu-5.0.0 ### 進入該目錄
$ ./configure --static --target-list=riscv64-softmmu,riscv64-linux-user ### 編譯前組態好QEMU,病設定目標處理器架構為"64位的RISC-V"
$ make -j16 ### 編譯,可用nproc查看可用的執行緒數
$ make install ### 安裝QEMU到默認位置

此處編譯qemu的時候,目標選擇了 riscv64-softmmu 和 riscv64-linux-user,兩者具體的區別為:

To put it simply, xxx-softmmu will compile qemu-system-xxx, which is an emulated machine for xxx architecture (System Emulation). When it resets, the starting point will be the reset vector of that architecture. While xxx-linux-user, compiles qemu-xxx, which allows you to run user application in xxx architecture (User-mode Emulation). Which will seek the user applications’ main function, and start execution from there.

安裝完畢後如果執行如下命令後能夠查看到qemu的具體版本,則說明安裝成功

$ qemu-system-riscv64 --version
QEMU emulator version 5.0.0
Copyright (c) 2003-2021 Fabrice Bellard and the QEMU Project developers
5.1.2.2 下載或編譯安裝 RISC‑V GCC Toolchain

途徑1(推薦):下載預編譯版本的 riscv-gnu-toolchain

  • 下載newlibc版本的riscv-gnu-toolchain,地址:https://www.sifive.com/software

這裡可選擇下載:GNU Embedded Toolchain — v2020.12.8 Ubuntu版本的 Prebuilt RISC‑V GCC Toolchain(帶有gdb偵錯工具)在此您也可以選擇運行在其它系統環境下的riscv-gnu-toolchain.

$ cd ~/riscv64_oslab
$ wget https://static.dev.sifive.com/dev-tools/freedom-tools/v2020.12/riscv64-unknown-elf-toolchain-10.2.0-2020.12.8-x86_64-linux-ubuntu14.tar.gz
$ tar -xzvf riscv64-unknown-elf-toolchain-10.2.0-2020.12.8-x86_64-linux-ubuntu14.tar.gz ###解壓
$ mv riscv64-unknown-elf-toolchain-10.2.0-2020.12.8-x86_64-linux-ubuntu14 \
     riscv64-unknown-elf-toolchain ### 重新命名資料夾
$ 編輯~/.bashrc 在檔案末尾新增如下環境變數設定語句
export PATH=~/riscv_oslab/riscv64-unknown-elf-toolchain/bin:$PATH
$ source ~/.bashrc
$ riscv64-unknown-elf-gcc --version ### 查看gcc版本
riscv64-unknown-elf-gcc (SiFive GCC-Metal 10.2.0-2020.12.8) 10.2.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  • 安裝 glibc 版本的 riscv-gnu-toolchain
$ sudo apt install binutils-riscv64-linux-gnu
$ sudo apt install gcc-riscv64-linux-gnu
$ riscv64-linux-gnu-gcc --version ### 查看gcc版本
riscv64-linux-gnu-gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

途徑2:從原始碼編譯獲得

首先下載 riscv-gnu-toolchain倉庫

$ git clone --recursive https://github.com/riscv/riscv-gnu-toolchain

注意:採用上述Git clone的方式來獲得riscv-gnu-toolchain可能需要6.65GB的空間

編譯安裝步驟:

./configure --prefix=/opt/riscv
make
5.1.2.3 使用上述交叉編譯工具鏈編譯Linux 5.10.42版本的核心
$ cd ~/riscv64_oslab
$ wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.10.42.tar.gz
$ tar -xzvf linux-5.10.42.tar.gz
$ cd linux-5.10.42
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- defconfig
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- -j $(nproc)
  ...
  GEN     .version
  CHK     include/generated/compile.h
  LD      vmlinux.o
  MODPOST vmlinux.symvers
  MODINFO modules.builtin.modinfo
  GEN     modules.builtin
  LD      .tmp_vmlinux.kallsyms1
  KSYMS   .tmp_vmlinux.kallsyms1.S
  AS      .tmp_vmlinux.kallsyms1.S
  LD      .tmp_vmlinux.kallsyms2
  KSYMS   .tmp_vmlinux.kallsyms2.S
  AS      .tmp_vmlinux.kallsyms2.S
  LD      vmlinux
  SYSMAP  System.map
  MODPOST modules-only.symvers
  GEN     Module.symvers
  OBJCOPY arch/riscv/boot/Image
  CC [M]  fs/efivarfs/efivarfs.mod.o
  GZIP    arch/riscv/boot/Image.gz
  LD [M]  fs/efivarfs/efivarfs.ko
  Kernel: arch/riscv/boot/Image.gz is ready
5.1.2.4 使用上述交叉編譯工具鏈編譯Busybox
$ cd ~/riscv64_oslab
$ wget https://busybox.net/downloads/busybox-1.33.1.tar.bz2
$ tar  -jxvf  busybox-1.33.1.tar.bz2 
$ cd busybox-1.33.1
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- defconfig
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- menuconfig ### 這裡打開後可以選擇直接exit,並保存到組態檔案.config中
$ 打開.config檔案並編輯新增 
  CONFIG_STATIC=y
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- -j $(nproc)
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- install
$ cd _install
$ mkdir proc sys dev etc etc/init.d
$ ls ### 查看目錄結構
bin  dev  etc  linuxrc  proc  sbin  sys  usr
$ touch etc/init.d/rcS 並編輯檔案內容為如下: ### We will now create a bash script in order to mount some devices automatically after the boot.
$ sudo mknod dev/console c 5 1
$ sudo mknod dev/ram b 1 0
#!bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
/sbin/mdev -s
$ chmod +x etc/init.d/rcS # Now change the file’s mode as executable.
$ find -print0 | cpio -0oH newc | gzip -9 > ../rootfs.img
$ cd ..
5.1.2.5 使用qemu 運行我們的linux kernel

從上述的實驗步驟中我們分別獲得了

~/riscv_oslab/busybox-1.33.1/rootfs.img
~/riscv_oslab/linux-5.10.42/arch/riscv/boot/Image

下面我們在QEMU中模擬運行我們地Linux核心:

執行如下命令列:

$ qemu-system-riscv64 \
        -nographic -machine virt \
        -kernel ~/riscv_oslab/linux-5.10.42/arch/riscv/boot/Image \
        -initrd ~/riscv_oslab/busybox-1.33.1/rootfs.img  \
        -append "root=/dev/ram rdinit=/sbin/init"

可以看到整個Linux Kernel地啟動流程:

OpenSBI v0.9
   ____                    _____ ____ _____
  / __ \                  / ____|  _ \_   _|
 | |  | |_ __   ___ _ __ | (___ | |_) || |
 | |  | | '_ \ / _ \ '_ \ \___ \|  _ < | |
 | |__| | |_) |  __/ | | |____) | |_) || |_
  \____/| .__/ \___|_| |_|_____/|____/_____|
        | |
        |_|

Platform Name             : riscv-virtio,qemu
Platform Features         : timer,mfdeleg
Platform HART Count       : 1
Firmware Base             : 0x80000000
Firmware Size             : 100 KB
Runtime SBI Version       : 0.2

Domain0 Name              : root
Domain0 Boot HART         : 0
Domain0 HARTs             : 0*
Domain0 Region00          : 0x0000000080000000-0x000000008001ffff ()
Domain0 Region01          : 0x0000000000000000-0xffffffffffffffff (R,W,X)
Domain0 Next Address      : 0x0000000080200000
Domain0 Next Arg1         : 0x0000000087000000
Domain0 Next Mode         : S-mode
Domain0 SysReset          : yes

Boot HART ID              : 0
Boot HART Domain          : root
Boot HART ISA             : rv64imafdcsu
Boot HART Features        : scounteren,mcounteren,time
Boot HART PMP Count       : 16
Boot HART PMP Granularity : 4
Boot HART PMP Address Bits: 54
Boot HART MHPM Count      : 0
Boot HART MHPM Count      : 0
Boot HART MIDELEG         : 0x0000000000000222
Boot HART MEDELEG         : 0x000000000000b109
[    0.000000] Linux version 5.10.42 (nn@ubuntu) (riscv64-linux-gnu-gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1 SMP Sat Jun 26 22:53:26 PDT 2021
[    0.000000] OF: fdt: Ignoring memory range 0x80000000 - 0x80200000
[    0.000000] efi: UEFI not found.
[    0.000000] Initial ramdisk at: 0x(____ptrval____) (1146880 bytes)
[    0.000000] Zone ranges:
[    0.000000]   DMA32    [mem 0x0000000080200000-0x0000000087ffffff]
[    0.000000]   Normal   empty
[    0.000000] Movable zone start for each node
[    0.000000] Early memory node ranges
[    0.000000]   node   0: [mem 0x0000000080200000-0x0000000087ffffff]
[    0.000000] Initmem setup node 0 [mem 0x0000000080200000-0x0000000087ffffff]
[    0.000000] software IO TLB: Cannot allocate buffer
[    0.000000] SBI specification v0.2 detected
[    0.000000] SBI implementation ID=0x1 Version=0x9
[    0.000000] SBI v0.2 TIME extension detected
[    0.000000] SBI v0.2 IPI extension detected
[    0.000000] SBI v0.2 RFENCE extension detected
[    0.000000] SBI v0.2 HSM extension detected
[    0.000000] riscv: ISA extensions acdfimsu
[    0.000000] riscv: ELF capabilities acdfim
[    0.000000] percpu: Embedded 17 pages/cpu s32360 r8192 d29080 u69632
[    0.000000] Built 1 zonelists, mobility grouping on.  Total pages: 31815
[    0.000000] Kernel command line: root=/dev/ram rdinit=/sbin/init
[    0.000000] Dentry cache hash table entries: 16384 (order: 5, 131072 bytes, linear)
[    0.000000] Inode-cache hash table entries: 8192 (order: 4, 65536 bytes, linear)
[    0.000000] Sorting __ex_table...
[    0.000000] mem auto-init: stack:off, heap alloc:off, heap free:off
[    0.000000] Memory: 108180K/129024K available (6954K kernel code, 4125K rwdata, 4096K rodata, 223K init, 342K bss, 20844K reserved, 0K cma-reserved)
[    0.000000] Virtual kernel memory layout:
[    0.000000]       fixmap : 0xffffffcefee00000 - 0xffffffceff000000   (2048 kB)
[    0.000000]       pci io : 0xffffffceff000000 - 0xffffffcf00000000   (  16 MB)
[    0.000000]      vmemmap : 0xffffffcf00000000 - 0xffffffcfffffffff   (4095 MB)
[    0.000000]      vmalloc : 0xffffffd000000000 - 0xffffffdfffffffff   (65535 MB)
[    0.000000]       lowmem : 0xffffffe000000000 - 0xffffffe007e00000   ( 126 MB)
[    0.000000] SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=1, Nodes=1
[    0.000000] rcu: Hierarchical RCU implementation.
[    0.000000] rcu: 	RCU restricting CPUs from NR_CPUS=8 to nr_cpu_ids=1.
[    0.000000] rcu: 	RCU debug extended QS entry/exit.
[    0.000000] 	Tracing variant of Tasks RCU enabled.
[    0.000000] rcu: RCU calculated value of scheduler-enlistment delay is 25 jiffies.
[    0.000000] rcu: Adjusting geometry for rcu_fanout_leaf=16, nr_cpu_ids=1
[    0.000000] NR_IRQS: 64, nr_irqs: 64, preallocated irqs: 0
[    0.000000] riscv-intc: 64 local interrupts mapped
[    0.000000] plic: plic@c000000: mapped 53 interrupts with 1 handlers for 2 contexts.
[    0.000000] random: get_random_bytes called from start_kernel+0x312/0x484 with crng_init=0
[    0.000000] riscv_timer_init_dt: Registering clocksource cpuid [0] hartid [0]
[    0.000000] clocksource: riscv_clocksource: mask: 0xffffffffffffffff max_cycles: 0x24e6a1710, max_idle_ns: 440795202120 ns
[    0.000155] sched_clock: 64 bits at 10MHz, resolution 100ns, wraps every 4398046511100ns
[    0.003256] Console: colour dummy device 80x25
[    0.004684] printk: console [tty0] enabled
[    0.008705] Calibrating delay loop (skipped), value calculated using timer frequency.. 20.00 BogoMIPS (lpj=40000)
[    0.008862] pid_max: default: 32768 minimum: 301
[    0.009925] Mount-cache hash table entries: 512 (order: 0, 4096 bytes, linear)
[    0.009977] Mountpoint-cache hash table entries: 512 (order: 0, 4096 bytes, linear)
[    0.031354] rcu: Hierarchical SRCU implementation.
[    0.033066] EFI services will not be available.
[    0.034680] smp: Bringing up secondary CPUs ...
[    0.034774] smp: Brought up 1 node, 1 CPU
[    0.042458] devtmpfs: initialized
[    0.047904] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645041785100000 ns
[    0.048157] futex hash table entries: 256 (order: 2, 16384 bytes, linear)
[    0.052753] NET: Registered protocol family 16
[    0.096960] vgaarb: loaded
[    0.097897] SCSI subsystem initialized
[    0.099470] usbcore: registered new interface driver usbfs
[    0.099708] usbcore: registered new interface driver hub
[    0.099841] usbcore: registered new device driver usb
[    0.109735] clocksource: Switched to clocksource riscv_clocksource
[    0.121388] NET: Registered protocol family 2
[    0.122636] IP idents hash table entries: 2048 (order: 2, 16384 bytes, linear)
[    0.125264] tcp_listen_portaddr_hash hash table entries: 128 (order: 0, 5120 bytes, linear)
[    0.125384] TCP established hash table entries: 1024 (order: 1, 8192 bytes, linear)
[    0.125566] TCP bind hash table entries: 1024 (order: 3, 32768 bytes, linear)
[    0.125704] TCP: Hash tables configured (established 1024 bind 1024)
[    0.126589] UDP hash table entries: 256 (order: 2, 24576 bytes, linear)
[    0.126838] UDP-Lite hash table entries: 256 (order: 2, 24576 bytes, linear)
[    0.127964] NET: Registered protocol family 1
[    0.130578] RPC: Registered named UNIX socket transport module.
[    0.130640] RPC: Registered udp transport module.
[    0.130666] RPC: Registered tcp transport module.
[    0.130690] RPC: Registered tcp NFSv4.1 backchannel transport module.
[    0.130796] PCI: CLS 0 bytes, default 64
[    0.132983] Unpacking initramfs...
[    0.184451] Freeing initrd memory: 1116K
[    0.186811] workingset: timestamp_bits=62 max_order=15 bucket_order=0
[    0.196540] NFS: Registering the id_resolver key type
[    0.197258] Key type id_resolver registered
[    0.197309] Key type id_legacy registered
[    0.197590] nfs4filelayout_init: NFSv4 File Layout Driver Registering...
[    0.197681] nfs4flexfilelayout_init: NFSv4 Flexfile Layout Driver Registering...
[    0.198313] 9p: Installing v9fs 9p2000 file system support
[    0.199295] NET: Registered protocol family 38
[    0.199533] Block layer SCSI generic (bsg) driver version 0.4 loaded (major 251)
[    0.199661] io scheduler mq-deadline registered
[    0.199745] io scheduler kyber registered
[    0.206411] pci-host-generic 30000000.pci: host bridge /soc/pci@30000000 ranges:
[    0.207040] pci-host-generic 30000000.pci:       IO 0x0003000000..0x000300ffff -> 0x0000000000
[    0.207407] pci-host-generic 30000000.pci:      MEM 0x0040000000..0x007fffffff -> 0x0040000000
[    0.207473] pci-host-generic 30000000.pci:      MEM 0x0400000000..0x07ffffffff -> 0x0400000000
[    0.209120] pci-host-generic 30000000.pci: ECAM at [mem 0x30000000-0x3fffffff] for [bus 00-ff]
[    0.209814] pci-host-generic 30000000.pci: PCI host bridge to bus 0000:00
[    0.210081] pci_bus 0000:00: root bus resource [bus 00-ff]
[    0.210175] pci_bus 0000:00: root bus resource [io  0x0000-0xffff]
[    0.210203] pci_bus 0000:00: root bus resource [mem 0x40000000-0x7fffffff]
[    0.210228] pci_bus 0000:00: root bus resource [mem 0x400000000-0x7ffffffff]
[    0.211207] pci 0000:00:00.0: [1b36:0008] type 00 class 0x060000
[    0.257315] Serial: 8250/16550 driver, 4 ports, IRQ sharing disabled
[    0.263577] 10000000.uart: ttyS0 at MMIO 0x10000000 (irq = 2, base_baud = 230400) is a 16550A
[    0.293271] printk: console [ttyS0] enabled
[    0.298044] [drm] radeon kernel modesetting enabled.
[    0.311076] loop: module loaded
[    0.313675] libphy: Fixed MDIO Bus: probed
[    0.314711] e1000e: Intel(R) PRO/1000 Network Driver
[    0.314950] e1000e: Copyright(c) 1999 - 2015 Intel Corporation.
[    0.315481] ehci_hcd: USB 2.0 'Enhanced' Host Controller (EHCI) Driver
[    0.315834] ehci-pci: EHCI PCI platform driver
[    0.316191] ehci-platform: EHCI generic platform driver
[    0.316510] ohci_hcd: USB 1.1 'Open' Host Controller (OHCI) Driver
[    0.316869] ohci-pci: OHCI PCI platform driver
[    0.317290] ohci-platform: OHCI generic platform driver
[    0.318730] usbcore: registered new interface driver uas
[    0.319250] usbcore: registered new interface driver usb-storage
[    0.320182] mousedev: PS/2 mouse device common for all mice
[    0.323094] goldfish_rtc 101000.rtc: registered as rtc0
[    0.323986] goldfish_rtc 101000.rtc: setting system clock to 2021-06-27T06:45:02 UTC (1624776302)
[    0.326296] syscon-poweroff soc:poweroff: pm_power_off already claimed (____ptrval____) sbi_shutdown
[    0.326805] syscon-poweroff: probe of soc:poweroff failed with error -16
[    0.327990] usbcore: registered new interface driver usbhid
[    0.328373] usbhid: USB HID core driver
[    0.330278] NET: Registered protocol family 10
[    0.337351] Segment Routing with IPv6
[    0.338095] sit: IPv6, IPv4 and MPLS over IPv4 tunneling driver
[    0.340452] NET: Registered protocol family 17
[    0.341736] 9pnet: Installing 9P2000 support
[    0.342188] Key type dns_resolver registered
[    0.342886] debug_vm_pgtable: [debug_vm_pgtable         ]: Validating architecture page table helpers
[    0.378742] Freeing unused kernel memory: 220K
[    0.381020] Run /sbin/init as init process

Please press Enter to activate this console. 

由於以安裝了Busy Box並製作了rootfs,因此可以運行較多的常用命令,示例如下:

/ # ls
bin      etc      proc     sbin     usr
dev      linuxrc  root     sys
/ # pwd
/ # cd bin
/bin # ls
arch           dumpkmap       kill           netstat        setarch
ash            echo           link           nice           setpriv
base32         ed             linux32        nuke           setserial
base64         egrep          linux64        pidof          sh
busybox        false          ln             ping           sleep
cat            fatattr        login          ping6          stat
chattr         fdflush        ls             pipe_progress  stty
chgrp          fgrep          lsattr         printenv       su
chmod          fsync          lzop           ps             sync
chown          getopt         makemime       pwd            tar
conspy         grep           mkdir          reformime      touch
cp             gunzip         mknod          resume         true
cpio           gzip           mktemp         rev            umount
cttyhack       hostname       more           rm             uname
date           hush           mount          rmdir          usleep
dd             ionice         mountpoint     rpm            vi
df             iostat         mpstat         run-parts      watch
dmesg          ipcalc         mt             scriptreplay   zcat
dnsdomainname  kbd_mode       mv             sed
/bin # 
退出QEMU模擬器的方法為:
使用ctrl+a(macOS下為control+a),鬆開後再按下x鍵即可退出qemu

Debug with GDB

接下來使用GDB來偵錯運行在QEMU中地Linux Kernel:

在之前運行QEMU啟動Linux Kernel的命令列之後追加如下兩個參數 -s -S

$ qemu-system-riscv64 \
        -nographic -machine virt \
        -kernel ~/riscv_oslab/linux-5.10.42/arch/riscv/boot/Image \
        -initrd ~/riscv_oslab/busybox-1.33.1/rootfs.img  \
        -append "root=/dev/ram rdinit=/sbin/init" \
        -s -S

然後打開另一個終端,使用 riscv64-unknown-elf-gdb 進行核心的偵錯工作:

查看gdb的版本號:

$ riscv64-unknown-elf-gdb --version
GNU gdb (SiFive GDB-Metal 10.1.0-2020.12.7) 10.1
$ riscv64-unknown-elf-gdb ~/riscv_oslab/linux-5.10.42/vmlinux

若gdb提示如下資訊:

Reading symbols from .../vmlinux…
(No debugging symbols found in .../vmlinux)

需要重新編譯核心,需要在核心Makefile的KBUILD_CFLAGS上新增**-g**選項,然後繼續運行上述命令列啟動gdb開始偵錯

在gdb中新增斷點,如在start_kernel處新增斷點後,輸⼊continue,則qemu會在初始化kernel的過程中停止,如下所示:

(gdb) remote target localhost:1234 ### 連接本地Qemu偵錯連接埠
(gdb) b start_kernel 
Breakpoint 1 at 0xffffffe00000272e
(gdb) continue
Continuing.
Breakpoint 1, 0xffffffe00000272e in start_kernel ()

Docker 安裝 Centos 7

docker search centos
docker pull centos:7.5.1804
docker run -itd --privileged=true -p 20010:22 --name="centos" centos:7.5.1804  /usr/sbin/init
docker exec -it centos bash
cat /etc/redhat-release
yum install java-1.8.0-openjdk  vim
java -version
  • vim /etc/yum.repos.d/cassandra.repo
[cassandra]
name=Apache Cassandra
baseurl=https://www.apache.org/dist/cassandra/redhat/311x/
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://www.apache.org/dist/cassandra/KEYS
yum update
yum install cassandra
systemctl enable cassandra &&  systemctl restart cassandra

查看
systemctl status cassandra
nodetool status
  • 刪除 cassandra
sudo rm -r /var/lib/cassandra
sudo rm -r /var/log/cassandra
sudo yum remove "cassandra-*"

TQDB

https://github.com/wldtw2008/tqdb/tree/prepareForCulster <==TQDB 在GitHub的最新版

https://github.com/wldtw2008/tqdb/blob/prepareForCulster/InitialTQDB.readme <==安裝說明

https://docs.datastax.com/en/cassandra-oss/3.x/cassandra/install/installRHEL.html <== cassandra 安裝

若不需自己安裝,我有已經裝好的VirtualBox的VM 在這裡 https://drive.google.com/open?id=16ZawNAWJNDcGV2jGirviIWzd_EwXlNfe id:tqdb, pw:tqdb@888, root pw:tqdb@888

遇到問題

  1. MC 8899 沒設定好 & 檢查設定

  2. 帳號路徑問題 需要依照帳號更改路徑 ex: trad 帳號

    find . -type f -exec sed -i 's|/home/tqdb|/home/trad|g'  {} \;
    
  3. 如果使用python3 script 需要更改因為開發是使用 python2

  4. 檢查process ps aux | grep 'tqdb'

使用不同 ssh 金鑰登入 github

在 github 抓取 Repository 時,我們常常用 git ssh 帳號去 clone 一個 Repository,像是:

git clone git@github.com:laravel/laravel.git

而使用 ssh 去 clone Repository 時,則會需要 ssh 金鑰 才能夠順利的將專案複製下來,但只要有正確的金鑰,我們在每一次對 Repository 進行 clone / push / pull / fetch 的時候,則都不需要輸入帳號密碼即可完成操作(只要你的帳號有足夠的權限的話)

但當我們有個人的專案及公司的專案都在 github 時,且不同的專案所需要的 ssh 金鑰 皆不同時,則需要設定在不同的狀況需要使用不同的金鑰去存取我們的 Repository。

例如 git@github.com:kj/kj.git 需要 id_rsa_kj_personal 的金鑰,但 git@github.com:kj-company/compony-project.git 則需要 id_rsa_kj_company 的金鑰

此時可以使用的解法有下列 2 個

設定 .ssh/config 檔案

.ssh/config 的設定檔案格式像下方

Host <host_alias>                       # 主機別名
    HostName <hostname_or_ip>           # 主機網址或 ip
    IdentityFile <private_key_path>     # 金鑰位置
git clone ssh://git@github.com-CryptoTrade/shihyu/CryptoTrade.git

所以我們可以將 .ssh/config 檔案設定成這樣

# GitHub KJ 個人專案
Host github-kj-personal
    HostName github.com
    IdentityFile ~/.ssh/id_rsa_kj_personal

# GitHub KJ 公司專案
Host github-kj-company
    HostName github.com
    IdentityFile ~/.ssh/id_rsa_kj_company

設定完 .ssh/config 之後

在存取個人專案的網址會從 git@github.com:kj/kj.git 改成 git@github-kj-personal:kj/kj.git

在存取公司專案的網址會從 git@github.com:kj-company/compony-project.git 改成 git@github-kj-company:kj-company/compony-project.git

所以複製專案指令會變成

git clone git@github-kj-personal:kj/kj.git
git clone git@github-kj-company:kj-company/compony-project.git

下列是 git ssh 網址格式說明,所以可以看到我們用 主機別名 Host <host_alias> 將原本的主機名稱改掉

# 原始網址
git@github.com:<accountname>/<reponame>.git

# 網址格式
git@<host_alias>:<accountname>/<reponame>.git

當我們存取 github-kj-personal 主機時,根據 .ssh/config 設定,我們會存取到設定的 HostNamegithub.com,使用的金鑰為 ~/.ssh/id_rsa_kj_personal

當我們存取 github-kj-company 主機時,根據 .ssh/config 設定,我們會存取到設定的 HostNamegithub.com,使用的金鑰為 ~/.ssh/id_rsa_kj_company

所以這樣設定可以讓我們同時對 github 使用不同的金鑰進行存取

加入臨時的 ssh 金鑰

在需要存取公司的 Repository 時,可以將公司的 ssh key 加入,這樣在一段時間內都可以使用此金耀進行存取

在 .bash_profile 可以設定指令的快捷

alias ssh-set-company-key='export GIT_SSH_COMMAND="ssh -i ~/.ssh/COMPANY_KEY";
                           export PS1="${PS1}COMPANY ==> "'

設定完指令 alias 後,之後需要使用到公司的金鑰時,就可以輸入此指令,就可以存取公司專案了

在 SourceTree 指定不同的金鑰

參考資料

  • https://kejyuntw.gitbooks.io/ubuntu-learning-notes/content/network/network-multiple-ssh-key-to-same-github-site.html

語言中的 Socket 是在寫網路程式必會碰到的東西,而它牽涉較多參數與函式,不如 python 簡潔,本篇用於紀錄關於網路程式中 socket 相關的細節


0x01 Berkeley Socket

  • Berkeley Socket 又稱 BSD Socket,是介於 Transport Layer 與 Application Layer 間的 API,用於行程間通訊 (UNIX Socket) 和網路通訊 (Network Socket)

  • Connection-oriented socket (TCP)

)

  • Connectionless socket (UDP)

)


0x02 通訊端 API 函式

  • **socket()** 根據指定的 socket type 建立一個新的 socket,為 socket 分配系統資源,並回傳一個 file descriptor
  • **bind()** 一般用於 server 端,用來將一個 socket file descriptor 和一個 sockaddr structure 相關聯,sockaddr 結構中會指出這個 socket (sockfd) 所要監聽的 address, port number 等資訊
  • **listen()** 用於 server 端,使一個 socket (sockfd) 進入監聽狀態
  • **connect()** 用於 client 端,他會透過 sockfd 和 sockaddr structure 向指定的 server 進行直接通訊,如果是連線導向的協定,如 TCP,則 connect function 會先建立起連線
  • **accept()** 用於 server 端,接受一個從 remote client 來的 TCP 連線請求,和 remote client 建立 TCP 連線並將建立的 socket 關連到 sockfd 與 sockaddr
  • **send()****recv()****write()****read()****recvfrom()****sendto()** 用於傳送與接收資料
  • **close()** 呼叫系統關閉分配的 sockfd,如果是 TCP 則連線會中斷
  • **gethostbyname()****gethostbyaddr()** 用來解析 hostname 和 address,IPv4 only

socket()

#include <sys/types.h>
#include <sys/socket.h>
 
int socket(int domain, int type, int protocol);
/* return a file descriptor for the new socket on success, or -1 if error */

建立一個 communication endpoint,並回傳一個 file descriptor

  • domain 為通訊端的協定集
    • AF_INET 表示 IPv4 網路協定
    • AF_INET6 表示 IPv6 網路協定
    • AF_UNIX 表示本地端通訊協定
  • type
    • SOCK_STREAM 提供雙向, 可靠的, 連線導向的串流連線 (TCP)
    • SOCK_DGRAM 提供非連線導向的 datagrams 類型 (UDP)
    • SOCK_SEQPACKET 提供雙向, 可靠的, 連線導向的 packet 類型
    • SOCK_RAW 在 Network Layer 上的原始網路協議
  • protocol 指定實際使用的傳輸協定,在 <netinet/in.h> 有詳細說明。 最常見的就是
    • IPPROTO_TCP
    • IPPROTO_SCTP
    • IPPROTO_UDP
    • IPPROTO_DCCP
    • 可以使用 0,即根據選定的domain和type選擇使用預設協定

bind()

#include <sys/socket.h>
 
int bind(int sockfd, const struct sockaddr *address, socklen_t address_len);
/* return 0 for successful; otherwise, -1 if error */
include <netinet/in.h>  
 
struct sockaddr {  
    unsigned short    sa_family;       // 2 bytes address family, AF_xxx  
    char              sa_data[14];     // 14 bytes of protocol address  
};  
 
// IPv4 AF_INET sockets:  
 
struct sockaddr_in {  
    short            sin_family;       // 2 bytes e.g. AF_INET, AF_INET6  
    unsigned short   sin_port;         // 2 bytes e.g. htons(3490)  
    struct in_addr   sin_addr;         // 4 bytes see struct in_addr, below  
    char             sin_zero[8];      // 8 bytes zero this if you want to  
};  
 
struct in_addr {  
    unsigned long s_addr;              // 4 bytes load with inet_pton()  
};  

用於 server 端,用來將一個 socket file descriptor 和一個 sockaddr structure 相關聯,sockaddr 結構中會指出這個 socket (sockfd) 所要監聽的 address, port number 等資訊

  • sockfd 為上面 socket() 回傳的 file descriptor
  • address 是一個 sockaddr 結構,包含了這個 socket 所要使用的一些資訊
    • sockaddr 和 sockaddr_in 結構類似,sockaddr_in 將 sockaddr 中的 char sa_data[14]; ,長度 14 bytes 轉為三個變數,一般寫成是我們使用 sockaddr_in 對其中的變數賦值,再將其轉型為 sockaddr
    • s_addr 是用 unsigned long int 所表示的 host address number
      • INADDR_LOOPBACK: 指本機的 address,也就是 127.0.0.1 (localhost)
      • INADDR_ANY: 指任何連上來的 address。如果要接受所有來自 internet 的 connection 可使用
      • INADDR_BROADCAST: 傳送 broadcast 訊息可使用
      • INADDR_NONE: 某些 function 錯誤時的回傳值
  • address_len 用來指出 sockaddr 結構長度

listen()

#include <sys/socket.h>
 
int listen(int sockfd, int backlog);
/* return 0 if success, otherwise, -1 for error */
  • sockfd 依然是上面的 socket file descriptor
  • backlog 指定監聽佇列大小,當有連線請求到來會進入此監聽佇列,連線請求被 accept() 後會離開監聽佇列,當佇列滿時,新的連線請求會返回錯誤

accept()

#include <sys/socket.h>
 
int accept(int sockfd, struct sockaddr *restrict address, socklen_t *restrict address_len);
/* return the non-negative file descriptor of the accepted socket if success, Otherwise, -1 if error */

接受一個監聽佇列中的連線,回傳指向 client 的 file descriptor

  • sockfd 依然是上面的 socket file descriptor
  • address 與上面 sockaddr 不同一個,自己宣告另一個指向 sockaddr structure 的變數用來記錄 client 的 socket 相關資訊,如不需要可以給 NULL
  • address_len 用來指出 sockaddr 結構長度,如果前面第二參數給 NULL ,則這邊也給 NULL

connect()

#include <sys/socket.h>
 
int connect(int sockfd, const struct sockaddr *address, socklen_t address_len);
/* return 0 if success, otherwise, -1 for error */

用於 client 端,他會透過 sockfd 和 sockaddr structure 向指定的 server 連線

  • sockfd client 一樣要呼叫 socket() 從回傳值取得
  • address 是一個 sockaddr 結構,包含了這個 socket 所要使用的一些資訊
  • address_len 用來指出 sockaddr 結構長度

0x03 Example code

/* Server code in C */
 
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
 
int main(void)
{
    struct sockaddr_in stSockAddr;
    int SocketFD = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
 
    if(-1 == SocketFD)
    {
        perror("can not create socket");
        exit(EXIT_FAILURE);
    }
 
    memset(&stSockAddr, 0, sizeof(struct sockaddr_in));
 
    stSockAddr.sin_family = AF_INET;
    stSockAddr.sin_port = htons(1100);
    stSockAddr.sin_addr.s_addr = INADDR_ANY;
 
    if(-1 == bind(SocketFD,(const struct sockaddr *)&stSockAddr, sizeof(struct sockaddr_in)))
    {
        perror("error bind failed");
        close(SocketFD);
        exit(EXIT_FAILURE);
    }
 
    if(-1 == listen(SocketFD, 10))
    {
        perror("error listen failed");
        close(SocketFD);
        exit(EXIT_FAILURE);
    }
 
    for(;;)
    {
        int ConnectFD = accept(SocketFD, NULL, NULL);
 
        if(0 > ConnectFD)
        {
            perror("error accept failed");
            close(SocketFD);
            exit(EXIT_FAILURE);
        }
 
        /* perform read write operations ... */
        shutdown(ConnectFD, SHUT_RDWR);
        close(ConnectFD);
    }
 
    close(SocketFD);
    return 0;
}
/* Client code in C */
 
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
 
int main(void)
{
    struct sockaddr_in stSockAddr;
    int Res;
    int SocketFD = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
 
    if (-1 == SocketFD)
    {
        perror("cannot create socket");
        exit(EXIT_FAILURE);
    }
 
    memset(&stSockAddr, 0, sizeof(struct sockaddr_in));
 
    stSockAddr.sin_family = AF_INET;
    stSockAddr.sin_port = htons(1100);
    Res = inet_pton(AF_INET, "192.168.1.3", &stSockAddr.sin_addr);
 
    if (0 > Res)
    {
        perror("error: first parameter is not a valid address family");
        close(SocketFD);
        exit(EXIT_FAILURE);
    }
    else if (0 == Res)
    {
        perror("char string (second parameter does not contain valid ipaddress");
        close(SocketFD);
        exit(EXIT_FAILURE);
    }
 
    if (-1 == connect(SocketFD, (const struct sockaddr *)&stSockAddr, sizeof(struct sockaddr_in)))
    {
        perror("connect failed");
        close(SocketFD);
        exit(EXIT_FAILURE);
    }
 
    /* perform read write operations ... */
 
    shutdown(SocketFD, SHUT_RDWR);
 
    close(SocketFD);
    return 0;
}

Hello Docker

  • Dockerfile
FROM python:3.7-slim

# Add requirements file in the container
COPY requirements.txt ./requirements.txt
RUN pip install -r requirements.txt

# Add source code in the container
COPY main.py ./main.py

# Define container entry point (could also work with CMD python main.py)
ENTRYPOINT ["python", "main.py"]

  • requirements.txt
requests==2.27.1
  • main.py
from pip import _internal

if __name__ == '__main__':
    print('Hello Docker world!')
    _internal.main(['list'])
docker build -t docker-python-helloworld .
docker images 
# docker create -i -t --name 光碟機  iso
docker create -i -t --name  docker_test docker-python-helloworld
docker ps
docker start -i docker_test
docker stop docker_test

Docker & Docker-compose 安裝

sudo apt install docker.io
sudo groupadd docker
sudo usermod -aG docker ${USER}
sudo chmod 666 /var/run/docker.sock
sudo service docker restart
需要退出重新登錄後才會生效



## docker-compose 
https://github.com/docker/compose/releases/
sudo curl -L "https://github.com/docker/compose/releases/download/v2.27.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod 755 docker-compose

Docker 教學

docker實際上,就是一個系統聯合幾個元件一直在欺騙一個處理程序,主要依靠了三個幫凶namespace,chroot,cgroup

Containers as a Service ( CaaS ) - 容器如同服務 Docker 是一個開源專案,出現於 2013 年初,最初是 Dotcloud 公司內部的 Side-Project。 它基於 Google 公司推出的 Go 語言實作。( Dotcloud 公司後來改名為 Docker )

Agenda

  • 基本介紹 - 映像檔、容器、倉庫
  • 指令說明 - 安裝、指令
  • Dockerfile 說明
  • 進階應用 - docker compose
  • 進階應用 - docker machine
  • 實際案例

基本介紹

什麼是容器技術Container: 應用程式為中心的虛擬化

Docker 歷史

1982年Unix系統內建的chroot機制 LXC 利用controler groups 與namespaces的功能, 提供應用軟體一個獨立的作業系統環境 2013 Linux之父Linus Torvalds 發布Linux核心3.8版 支援Container技術 2013 dotCloud公司將內部專案Docker開源釋出程式碼

Containers(容器) vs Virtual Machines(虛擬主機)

螢幕快照 2019-02-19 上午11.07.39.png

Docker 三個基本概念

映像檔(Image)
  • Docker 映像檔就是一個唯讀的模板。
  • 映像檔可以用來建立 Docker 容器。
容器(Container)
  • 容器是從映像檔建立的執行實例。
  • Docker 利用容器來執行應用。
  • 可以被啟動、開始、停止、刪除。
  • 每個容器都是相互隔離的、保證安全的平臺。
倉庫(Repository)
  • 倉庫是集中存放映像檔檔案的場所。
  • 每個倉庫中又包含了多個映像檔。
  • 每個映像檔有不同的標籤(tag)。
  • 倉庫分為公開倉庫(Public)和私有倉庫(Private)兩種形式。

螢幕快照 2019-02-19 上午11.10.38.png

指令說明 - 安裝、指令

docker --help

Containers(容器) vs Virtual Machines(虛擬主機)

螢幕快照 2019-02-19 上午11.07.39.png

Docker 三個基本概念

映像檔(Image)
  • Docker 映像檔就是一個唯讀的模板。
  • 映像檔可以用來建立 Docker 容器。
容器(Container)
  • 容器是從映像檔建立的執行實例。
  • Docker 利用容器來執行應用。
  • 可以被啟動、開始、停止、刪除。
  • 每個容器都是相互隔離的、保證安全的平臺。
倉庫(Repository)
  • 倉庫是集中存放映像檔檔案的場所。
  • 每個倉庫中又包含了多個映像檔。
  • 每個映像檔有不同的標籤(tag)。
  • 倉庫分為公開倉庫(Public)和私有倉庫(Private)兩種形式。

螢幕快照 2019-02-19 上午11.10.38.png

指令說明 - 安裝、指令

docker --help

安裝Docker

官方文件 Get started with Docker for Mac 官方官方 Get started with Docker for Windows Docker Toolbox overview

Image 映像檔 常用指令

指令說明範例
search搜尋docker search centos
pull下載docker pull centos
images列表docker images
run執行docker run -ti centos /bin/bash
rmi [Image ID]刪除docker rmi 615cb40d5d19
build建立docker build -t member:1 .
login登入docker login docker.okborn.com
push上傳docker push
Search 搜尋 Centos 映像檔
docker search contos

螢幕快照 2019-02-19 下午1.45.28.png

NAME:映像檔名稱 DESCRIPTION:映像檔描述 STARS:越多代表越多人使用 OFFICIAL:官方Image AUTOMATED:自動化

顯示目前本機的 Images 列表
docker iamges

REPOSITORY:倉庫位置和映像檔名稱 TAG:映像檔標籤(通常是定義版本號) IMAGE ID:映像檔ID(唯一碼) CREATED:創建日期 SIZE:映像檔大小

啟動容器
docker run -ti centos /bin/bash

run : 參數說明 or docker run --help 常用: -i :則讓容器的標準輸入保持打開 -t:讓Docker分配一個虛擬終端(pseudo-tty)並綁定到容器的標準輸入上 -d:背景執行 -e:設定環境變數(AAA=BBB) -p:Port 對應(host port:container port) -v:資料對應(host folder:container folder) --name:設定容器名稱

** 在執行RUN 映像檔時,如果沒有下載會先下載在執行 ** 螢幕快照 2019-02-19 下午2.00.23.png

rmi : 刪除映像檔前要先移除所有Container build : 使用build 指令時要先切換到Dockerfile 目錄下面

Container 容器 常用指令

指令說明範例
run新建或啟動docker run -d centos
start [Contain ID]啟動docker start a469b9226fc8
stop [Contain ID]停止docker stop a469b9226fc8
rm [Contain ID]刪除docker rm a4
ps -a列表docker ps -a
logs [Contain ID]查看容器內的資訊docker logs -f a4
exec [Contain ID]進入容器(開新console)docker exec -ti a4 /bin/bash
attach進入容器(退出停止容器)dockr attach a4
inspect查看docker inspect a4
啟動一個 Container 並且執行 ping google.com
docker run centos ping google.com
請動一個 Container 執行上面的動作,並背景執行

螢幕快照 2019-02-19 下午2.38.01.png

使用 查看 Container 指令
docker ps 
docker ps -a

ps : 參數說明 or docker ps --help 常用: -a:顯示全部的容器

CONTAINER ID:容器ID IMAGE:映像檔名稱 COMMAND:執行指令 CREATED:創建時間 STATUS:容器狀態 POSTS:開啟的Port號 NAMES:容器名稱

顯示容器的 log
docker logs -f 8a

logs : 參數說明 or docker logs --help 常用: -f:不會跳出,會一直列印最新的log資訊 螢幕快照 2019-02-19 下午2.48.58.png

進入容器
docker exec -ti 8a /bin/bash

exec : 參數說明 or docker exec --help 常用: -i :則讓容器的標準輸入保持打開 -t:讓Docker分配一個虛擬終端(pseudo-tty)並綁定到容器的標準輸入上 -e:設定環境變數(AAA=BBB) 螢幕快照 2019-02-19 下午2.51.57.png

查看容器資訊
docker inspect 8a

螢幕快照 2019-02-19 下午2.56.52.png

開啟容器到關閉容器
docker run -d ubuntu:14.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
db0e9dbb150596a3a89db056d0ecb765c54c3c2fb5d428e3b35fc20b55813862
docker logs -f db
docker ps -a
docker stop db
docker ps -a

Registry 倉庫 常用指令

指令說明範例
commit容器存檔docker commit db aaa:v1
pull下載docker pull docker.okborn.com/okborn:base
tag標籤docker tag aaa docker.okborn.com/aaa
push上傳docker push docker.okborn.com/member:1
login登入docker login docker.okborn.com
export匯出docker export 7691a814370e > ubuntu.tar
import匯入cat ubuntu.tar sudo docker import - test/ubuntu:v1.0
對容器存檔
docker run -d ubuntu:14.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
96ea2a3f99e92ddd5fa0ec29f21d035703b6512f59c4f54fbaee551ee8fc044a
docker commit 96 aaa:v1

螢幕快照 2019-02-19 下午3.31.21.png

對映像檔打標籤
docker tag centos aaa asia.gcr.io/joyi-205504/aaa:v1
上傳到 GCP Registry
gcloud docker -- push  asia.gcr.io/joyi-205504/aaa:v1

其他常用指令

刪除

docker rmi `docker images|grep sele |awk '{print $3}'`

Dcoker 資料管理

資料卷(Data volumes)
  • 資料卷可以在容器之間共享和重用
  • 對資料卷的修改會立馬生效
  • 對資料卷的更新,不會影響映像檔
  • 卷會一直存在,直到沒有容器使用
範例:建立一個 web 容器,並載入一個資料捲到容器的 /webapp 目錄
docker run -d -P --name web -v /webapp training/webapp python app.py
範例:本機的 /src/webapp 目錄到容器的 /opt/webapp 目錄
docker run -d -P --name web -v /src/webapp:/opt/webapp training/webapp python app.py
範例:Docker 掛載資料卷的預設權限是讀寫,使用者也可以透過 :ro 指定為唯讀
docker run -d -P --name web -v /src/webapp:/opt/webapp:ro training/webapp python app.py
資料卷容器(Data volume containers)

持續更新的資料需要在容器之間共享,最好建立資料卷容器。 一個正常的容器,專門用來提供資料卷供其它容器掛載的。

範例:建立一個命名的資料卷容器 dbdata
docker run -d -v /dbdata --name dbdata postgres echo Data-only container for postgres
範例:他容器中使用 --volumes-from 來掛載 dbdata 容器中的資料卷
docker run -d -P --volumes-from dbdata --name db1 postgres
docker run -d -P --volumes-from dbdata --name db2 postgres
範例:也可以從其他已經掛載了容器卷的容器來掛載資料卷。
docker run -d --name db3 --volumes-from db1 postgres
範例:備份

首先使用 --volumes-from 標記來建立一個載入 dbdata 容器卷的容器,並從本地主機掛載當前到容器的 /backup 目錄。

docker run --volumes-from dbdata -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata
範例:恢復

恢復資料到一個容器,首先建立一個帶有資料卷的容器 dbdata2

docker run -v /dbdata --name dbdata2 ubuntu /bin/bash

然後建立另一個容器,掛載 dbdata2 的容器,並使用 untar 解壓備份檔案到掛載的容器卷中。

docker run --volumes-from dbdata2 -v $(pwd):/backup busybox tar xvf /backup/backup.tar
Docker 中的網路功能介紹
  • 要讓外部也可以存取這些應用
  • 可以通過 -P 或 -p 參數來指定連接埠映射。
範例:隨機本機Port
docker run -d -P training/webapp python app.py
範例:指定本機Port
docker run -d -p 5000:5000 training/webapp python app.py
範例:綁定 localhost 的任意連接埠到容器的 5000 連接埠,本地主機會自動分配一個連接埠
docker run -d -p 127.0.0.1::5000 training/webapp python app.py
範例:還可以使用 udp 標記來指定 udp 連接埠
docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py
範例: -p 標記可以多次使用來綁定多個連接埠
docker run -d -p 5000:5000  -p 3000:80 training/webapp python app.py

Dockerfile 說明

  • Dockerfile 由一行行命令語句組成,並且支援以 # 開頭的註解行。
  • Dockerfile 分為四部分:
    • 基底映像檔資訊
    • 維護者資訊
    • 映像檔操作指令
    • 容器啟動時執行指令。
# This dockerfile uses the ubuntu image
# VERSION 2 - EDITION 1
# Author: docker_user
# Command format: Instruction [arguments / command] ..

# 基本映像檔,必須是第一個指令
FROM ubuntu

# 維護者: docker_user <docker_user at email.com> (@docker_user)
MAINTAINER docker_user docker_user@email.com

# 更新映像檔的指令
RUN echo "deb http://archive.ubuntu.com/ubuntu/ raring main universe" >> /etc/apt/sources.list
RUN apt-get update && apt-get install -y nginx
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf

# 建立新容器時要執行的指令
CMD /usr/sbin/nginx

####Dockerfile 基本語法

指令說明範例
FROM :映像檔來源FROM python:3.5
MAINTAINER維護者訊息MAINTAINER docker_user docker_user@email.com
RUN創建映像檔時執行動作RUN apt-get -y update && apt-get install -y supervisor
RUN ["executable", "param1", "param2"]創建映像檔時執行動作RUN ["/bin/bash", "-c", "echo hello"]
CMD command param1 param2啟動容器時執行的命令CMD pserve development.ini
CMD ["executable","param1","param2"]啟動容器時執行的命令
CMD ["param1","param2"]啟動容器時執行的命令
EXPOSE容器對外的埠號EXPOSE 8082
ADD複製檔案(單檔)ADD requirements.txt /usr/src/app/
COPY複製檔案(資料夾)COPY . /usr/src/app
ENV環境變數ENV PG_VERSION 9.3.4
ENTRYPOINT command param1 param2指定容器啟動後執行的命令
ENTRYPOINT ["executable", "param1", "param2"]指定容器啟動後執行的命令ENTRYPOINT ["/docker-entrypoint.sh"]
VOLUME ["/data"]掛載資料卷VOLUME /var/lib/postgresql/data
USER daemon指定運行使用者RUN groupadd -r postgres && useradd -r -g postgres postgres
WORKDIR /path/to/workdir指定工作目錄WORKDIR /usr/src/app
ONBUILD [INSTRUCTION]基底映像檔建立時執行ONBUILD COPY . /usr/src/app

RUN 當命令較長時可以使用 \ 來換行。 RUN : 在 shell 終端中運行命令,即 /bin/sh -c; RUN ["executable", "param1", "param2"] : 使用 exec 執行。

CMD 指定啟動容器時執行的命令, 每個 Dockerfile 只能有一條 CMD 命令 。 如果指定了多條命令,只有最後一條會被執行。 CMD ["executable","param1","param2"] 使用 exec 執行,推薦使用; CMD command param1 param2 在 /bin/sh 中執行,使用在給需要互動的指令; CMD ["param1","param2"] 提供給 ENTRYPOINT 的預設參數;

ENTRYPOINT:每個 Dockerfile 中只能有一個 ENTRYPOINT,當指定多個時,只有最後一個會生效。 USER:要臨時取得管理員權限可以使用 gosu,而不推薦 sudo。 WORKDIR:可以使用多個 WORKDIR 指令,後續命令如果參數是相對路徑,則會基於之前命令指定的路徑

Docker File Base

# 映像檔Image
FROM python:3.5
# 維護者
MAINTAINER Pellok "pellok@double-cash.com"
# 更新
RUN apt-get -y update && apt-get install -y supervisor
# 創建專案資料夾
RUN mkdir -p /usr/src/app
# 指定工作目錄在專案資料夾
WORKDIR /usr/src/app
# 預先要安裝的requirements複製到Docker裡面
ADD requirements.txt /usr/src/app/
# 安裝需要用的插件
RUN pip install --upgrade pip setuptools
RUN pip install --no-cache-dir -r requirements.txt
# 下次Build 的時候複製專案目錄到Docker 裡面
ONBUILD COPY . /usr/src/app

建置

docker build -t sample:base .
Docker File for Project
#  挑選Image
FROM sample:base
# 安裝cryptography
RUN pip install cryptography 
# 設定工作目錄
WORKDIR /usr/src/app/
# 執行Python Setup
RUN python setup.py develop
# 開啟Port號
EXPOSE 8082
# 執行專案
CMD pserve development.ini

建置

docker build -t project:v1 .

Pyramid 專案 Docker 化

#創建一個新專案
pcreate -s alchemy pyramid_dockerlize
cd pyramid_dockerlize
# 創建dockerfile
touch Dockerfile
# 編輯 Dockerfile
# 建置映像檔
docker build -t pyramid_dockerlize .
# 執行容器
docker run -d -P pyramid_dockerlize

Dockerfile

# This dockerfile uses the python pyramid
# VERSION 1 - EDITION 1
# Author: pellok
# Command describe

# 使用的python映像檔版本
FROM python:3.5

MAINTAINER pellok pellok@okborn.com

# 創建存放專案的資料夾
RUN mkdir -p /usr/src/app

# 複製當前目錄的所有檔案到容器內的,資料放在/usr/src/app
COPY . /usr/src/app

# 指定工作目錄
WORKDIR /usr/src/app/

# 安裝環境變數和相依性套件
RUN python setup.py develop

# 初始化DB
RUN initialize_pyramid_dockerlize_db development.ini

# 專案監聽的Port號
EXPOSE 6543

# 啟動專案
CMD pserve production.ini

要在 Docker 中安裝 Ubuntu 24.04 並登入 Bash,然後安裝 pippythonvimunzipwget,你可以按照以下步驟操作:

1. 建立並啟動 Docker 容器

首先,在終端機中執行以下命令來從 Docker Hub 拉取 Ubuntu 24.04 的映像檔,並建立一個容器:

docker pull ubuntu:24.04
docker run -it --name ubuntu-container ubuntu:24.04 bash

這會將你帶入 Ubuntu 容器的 Bash 環境中。

2. 更新軟體包

在容器內,首先更新軟體包列表:

apt update

3. 安裝 Python, pip, vim, unzip, wget

接下來,安裝所需的軟體包:

apt install -y python3 python3-pip vim unzip wget

4. 驗證安裝

確認安裝是否成功:

python3 --version
pip3 --version
vim --version
unzip -v
wget --version

這些指令應該顯示安裝的版本資訊。

5. 保存和退出 Docker 容器

完成後,可以保存和退出容器:

exit

6. 再次進入容器

你可以隨時再次進入容器:

docker start ubuntu-container
docker exec -it ubuntu-container bash

這樣你就能再次進入並使用已安裝的軟體。

這個錯誤訊息是由於 Ubuntu 24.04 預設使用 "externally managed environment" 來管理 Python 軟體包,這意味著你不能直接使用 pip 在系統範圍內安裝 Python 軟體包(例如在全域範圍內)。這是因為使用 pip 安裝軟體包可能會和系統管理的 Python 軟體包產生衝突。

解決方案

你可以有以下幾個選項來解決這個問題:

1. 使用 apt 安裝 Python 軟體包

如果你需要安裝 Python 軟體包,建議使用 Ubuntu 的套件管理工具 apt

apt install python3-pip

這樣可以避免和系統衝突,因為 apt 會使用預先打包好的軟體包版本。

2. 創建一個虛擬環境

如果你需要使用 pip 安裝一些系統沒有的 Python 軟體包,建議創建一個虛擬環境:

apt install python3-venv  # 首先安裝虛擬環境模組
python3 -m venv myenv     # 創建虛擬環境
source myenv/bin/activate # 啟用虛擬環境

在啟用虛擬環境後,使用 pip 安裝軟體包:

pip install <package_name>

當你不再需要虛擬環境時,可以執行 deactivate 來退出。

3. 使用 pipx 來安裝應用程式

如果你想安裝一個獨立的 Python 應用程式,可以使用 pipx 來安裝。首先安裝 pipx

apt install pipx
pipx ensurepath

然後使用 pipx 安裝應用程式:

pipx install <application_name>

4. 使用 --break-system-packages 強制安裝(不推薦)

你也可以強制繞過這個限制,但這可能會導致系統不穩定或產生衝突,不推薦這樣做:

pip install <package_name> --break-system-packages

這種方式應謹慎使用,只適合那些了解其風險的用戶。

建議

建議使用虛擬環境 (venv) 或 pipx,這樣可以避免破壞系統環境,同時也能滿足你對 Python 軟體包的安裝需求。

參考

VM-for-Devops Virtual Box [VirtualBox5.1.8][Extension Pack] Vagrant[Vagrant1.8.7] kubernetes minikube

在dockerfile中設定時區

基於 Debian 鏡像

由於 Debian 鏡像中已經包含了tzdata,因此設定時區的方法比較簡單,只需新增環境變數TZ即可。

FROM debian:stretch

ENV TZ=Asia/Taipei

基於 Alpine 鏡像

FROM alpine:3.9

ENV TZ=Asia/Taipei

RUN apk update \
    && apk add tzdata \
    && echo "${TZ}" > /etc/timezone \
    && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \
    && rm /var/cache/apk/*

基於 Ubuntu 鏡像

FROM ubuntu:bionic

ENV TZ=Asia/Taipei

RUN echo "${TZ}" > /etc/timezone \
    && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \
    && apt update \
    && apt install -y tzdata \
    && rm -rf /var/lib/apt/lists/*

簡單範例

from pip import _internal
import time

if __name__ == '__main__':
    _internal.main(['list'])
    while True:
        print('Hello Docker world!')
        time.sleep(1)
FROM python:3.10-slim

# Add requirements file in the container
COPY requirements.txt ./requirements.txt
RUN pip install -r requirements.txt

# Add source code in the container
COPY main.py ./main.py

# Define container entry point (could also work with CMD python main.py)
ENTRYPOINT ["python", "main.py"]
  • requirements.txt
requests
kafka-python
grpcio
protobuf
better-exceptions
loguru
pandas
python-binance
redis
aiohttp
flask
kubernetes
# 編譯 image
docker build -t my-image-name .  

# Run docker
docker run -it my-image-name

#查看 container_name or id
docker ps
79c7e6661fa2   my-image-name                     "python main.py"         54 seconds ago   Up 54 seconds                                                                                                    beautiful_khayyam

docker exec -it 79c7e6661fa2 /bin/bash
docker exec -it beautiful_khayyam /bin/bash

  1. 使用命令行將上述 Dockerfile 文件保存在您的計算機上。

  2. 創建 Docker 映像,請在命令行中導航到 Dockerfile 文件所在的目錄,並運行以下命令:

    docker build -t image_name .
    

    其中,image_name 是您要為映像命名的名稱,. 表示當前目錄是上下文。

  3. 運行 Docker 映像,請使用以下命令:

    docker run -it --rm image_name
    

    其中,-it 表示要使用互動式終端來運行容器,--rm 表示當容器停止時,自動刪除容器。

  4. 如果需要,在運行容器的情況下登入 Docker,請使用以下命令:

    docker exec -it container_name /bin/bash
    

    其中,container_name 是您要登入的容器的名稱,/bin/bash 是您要在容器中運行的命令。

希望這些命令可以幫助您成功編譯、運行和登入您的 Docker 映像。

Creating the Perfect Python Dockerfile

# using ubuntu LTS version
FROM ubuntu:20.04 AS builder-image

# avoid stuck build due to user prompt
ARG DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install --no-install-recommends -y python3.9 python3.9-dev python3.9-venv python3-pip python3-wheel build-essential && \
	apt-get clean && rm -rf /var/lib/apt/lists/*

# create and activate virtual environment
# using final folder name to avoid path issues with packages
RUN python3.9 -m venv /home/myuser/venv
ENV PATH="/home/myuser/venv/bin:$PATH"

# install requirements
COPY requirements.txt .
RUN pip3 install --no-cache-dir wheel
RUN pip3 install --no-cache-dir -r requirements.txt

FROM ubuntu:20.04 AS runner-image
RUN apt-get update && apt-get install --no-install-recommends -y python3.9 python3-venv && \
	apt-get clean && rm -rf /var/lib/apt/lists/*

RUN useradd --create-home myuser
COPY --from=builder-image /home/myuser/venv /home/myuser/venv

USER myuser
RUN mkdir /home/myuser/code
WORKDIR /home/myuser/code
COPY . .

EXPOSE 5000

# make sure all messages always reach console
ENV PYTHONUNBUFFERED=1

# activate virtual environment
ENV VIRTUAL_ENV=/home/myuser/venv
ENV PATH="/home/myuser/venv/bin:$PATH"

# /dev/shm is mapped to shared memory and should be used for gunicorn heartbeat
# this will improve performance and avoid random freezes
CMD ["gunicorn","-b", "0.0.0.0:5000", "-w", "4", "-k", "gevent", "--worker-tmp-dir", "/dev/shm", "app:app"]

https://luis-sena.medium.com/creating-the-perfect-python-dockerfile-51bdec41f1c8

由 Docker image 反推其 Dockerfile

當我們使用現成的 Docker image 進行開發時,有時候會想知道這個 image 的內容是什麼,他提供的功能是如何達成的,這個時候如果能找到作者提供的 Dockerfile 是最好,但如果對方沒有公開的話,就有點麻煩了,這時候我們可以使用內建的 docker history 指令根據每層 image layer 的 metadata 看出做的事情,此外也有大大做了現成的工具讓我們可以直接產出接近原本 Dockerfile 該有的內容。

這邊我拿了兩個工具來做實驗:

1. https://github.com/CenturyLinkLabs/dockerfile-from-image
2. https://github.com/lukapeschke/dockerfile-from-image

其中只有第二個是可行的,以下是實驗過程。

首先這個工具是參考這篇而看到的。 我先拿 ruby 嘗試一下:

$ docker pull centurylink/dockerfile-from-image
$ docker pull ruby
$ docker image | grep ruby
REPOSITORY                                    TAG                 IMAGE ID            CREATED              SIZE
docker.io/ruby                                latest              d529acb9f124        4 weeks ago          840 MB
$ docker run -v /var/run/docker.sock:/var/run/docker.sock centurylink/dockerfile-from-image d529acb9f124
/usr/lib/ruby/gems/2.2.0/gems/docker-api-1.24.1/lib/docker/connection.rb:42:in `rescue in request': 400 Bad Request: malformed Host header (Docker::Error::ClientError)
        from /usr/lib/ruby/gems/2.2.0/gems/docker-api-1.24.1/lib/docker/connection.rb:38:in `request'
        from /usr/lib/ruby/gems/2.2.0/gems/docker-api-1.24.1/lib/docker/connection.rb:65:in `block (2 levels) in <class:Connection>'
        from /usr/lib/ruby/gems/2.2.0/gems/docker-api-1.24.1/lib/docker/image.rb:172:in `all'
        from /usr/src/app/dockerfile-from-image.rb:32:in `<main>'

發現也有其他人碰到相同問題,參考這邊的說明,利用他給的 Dockerfile 重新 build image 之後,反倒無法輸出任何東西。這個 repository 最近一次更新也是 2015 的事了,該工具似已不再適用新版的 Docker。

參考上面同的討論串後續的內容找到第二個 repository,用他的 Dockerfile 來 build image:

$ git clone https://github.com/lukapeschke/dockerfile-from-image.git
$ cd dockerfile-from-image/
$ docker build --rm -t lukapeschke/dockerfile-from-image .

他的使用方法 (只能用 image ID,不能用 image name!):

$ docker run --rm -v '/var/run/docker.sock:/var/run/docker.sock' lukapeschke/dockerfile-from-image <IMAGE_ID>

以下拿 ruby 測試可以順利產生我們要的:

$ docker run --rm -v '/var/run/docker.sock:/var/run/docker.sock' lukapeschke/dockerfile-from-image d529acb9f124
FROM docker.io/ruby:latest
ADD file:2cddee716e84c40540a69c48051bd2dcf6cd3bd02a3e399334e97f20a77126ff in /
CMD ["bash"]
RUN /bin/sh -c apt-get update \
    && apt-get install -y --no-install-recommends 		ca-certificates 		curl 		netbase 		wget 	\
    && rm -rf /var/lib/apt/lists/*
RUN /bin/sh -c set -ex; 	if ! command -v gpg > /dev/null; then 		apt-get update; 		apt-get install -y --no-install-recommends 			gnupg 			dirmngr 		; 		rm -rf /var/lib/apt/lists/*; 	fi
RUN /bin/sh -c apt-get update \
    && apt-get install -y --no-install-recommends 		git 		mercurial 		openssh-client 		subversion 	procps 	\
    && rm -rf /var/lib/apt/lists/*
RUN /bin/sh -c set -ex; 	apt-get update; 	apt-get install -y --no-install-recommends 		autoconf 		automake 		bzip2 		dpkg-dev 		file 		g++ 		gcc 		imagemagick 		libbz2-dev 	libc6-dev 		libcurl4-openssl-dev 		libdb-dev 		libevent-dev 		libffi-dev 		libgdbm-dev 	libgeoip-dev 		libglib2.0-dev 		libgmp-dev 		libjpeg-dev 		libkrb5-dev 		liblzma-dev 		libmagickcore-dev 		libmagickwand-dev 		libncurses5-dev 		libncursesw5-dev 		libpng-dev 	libpq-dev 		libreadline-dev 		libsqlite3-dev 		libssl-dev 		libtool 		libwebp-dev 	libxml2-dev 		libxslt-dev 		libyaml-dev 		make 		patch 		unzip 		xz-utils 		zlib1g-dev 				$( 			if apt-cache show 'default-libmysqlclient-dev' 2>/dev/null | grep -q '^Version:'; then 				echo 'default-libmysqlclient-dev'; 			else 				echo 'libmysqlclient-dev'; 			fi 		) 	; 	rm -rf /var/lib/apt/lists/*
RUN /bin/sh -c set -eux; 	mkdir -p /usr/local/etc; 	{ 		echo 'install: --no-document'; 		echo 'update: --no-document'; 	} >> /usr/local/etc/gemrc
ENV RUBY_MAJOR=2.6
ENV RUBY_VERSION=2.6.3
ENV RUBY_DOWNLOAD_SHA256=11a83f85c03d3f0fc9b8a9b6cad1b2674f26c5aaa43ba858d4b0fcc2b54171e1
RUN /bin/sh -c set -eux; 		savedAptMark="$(apt-mark showmanual)"; 	apt-get update; 	apt-get install -y --no-install-recommends 		bison 		dpkg-dev 		libgdbm-dev 		ruby 	; 	rm -rf /var/lib/apt/lists/*; 		wget -O ruby.tar.xz "https://cache.ruby-lang.org/pub/ruby/${RUBY_MAJOR%-rc}/ruby-$RUBY_VERSION.tar.xz"; 	echo "$RUBY_DOWNLOAD_SHA256 *ruby.tar.xz" | sha256sum --check --strict; 		mkdir -p /usr/src/ruby; 	tar -xJf ruby.tar.xz -C /usr/src/ruby --strip-components=1; 	rm ruby.tar.xz; 		cd /usr/src/ruby; 		{ 		echo '#define ENABLE_PATH_CHECK 0'; 		echo; 		cat file.c; 	} > file.c.new; 	mv file.c.new file.c; 		autoconf; 	gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; 	./configure 		--build="$gnuArch" 		--disable-install-doc 		--enable-shared 	; 	make -j "$(nproc)"; 	make install; 		apt-mark auto '.*' > /dev/null; 	apt-mark manual $savedAptMark > /dev/null; 	find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec ldd '{}' ';' 		| awk '/=>/ { print $(NF-1) }' 		| sort -u 		| xargs -r dpkg-query --search 		| cut -d: -f1 		| sort -u 		| xargs -r apt-mark manual 	; 	apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; 		cd /; 	rm -r /usr/src/ruby; 	! dpkg -l | grep -i ruby; 	[ "$(command -v ruby)" = '/usr/local/bin/ruby' ]; 	ruby --version; 	gem --version; 	bundle --version
ENV GEM_HOME=/usr/local/bundle
ENV BUNDLE_PATH=/usr/local/bundle BUNDLE_SILENCE_ROOT_WARNING=1 BUNDLE_APP_CONFIG=/usr/local/bundle
ENV PATH=/usr/local/bundle/bin:/usr/local/bundle/gems/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
RUN /bin/sh -c mkdir -p "$GEM_HOME" \
    && chmod 777 "$GEM_HOME"
CMD ["irb"]

不過可惜的是像這樣的工具終究無法還原出真實 Dockerfile 中 COPY 或 ADD 的時候加入到 image 中的檔案。 例如以下是用該工具還原他自己:

$ docker images | grep "lukapeschke/dockerfile-from-image"
lukapeschke/dockerfile-from-image             latest              d719f8dcb798        37 minutes ago      59 MB

$ docker run -v /var/run/docker.sock:/var/run/docker.sock lukapeschke/dockerfile-from-image d719f8dcb798
FROM docker.io/alpine:latest
RUN /bin/sh -c apk add --update python3 wget      \
    && wget -O - --no-check-certificate https://bootstrap.pypa.io/get-pip.py | python3      \
    && apk del wget      \
    && pip3 install -U docker-py      \
    && yes | pip3 uninstall pip
COPY file:d7369c0379dc34ec79c308a782b14eab9c86ed1ebc41b5ce859e32760518fb21 in /root
ENTRYPOINT ["/root/entrypoint.py"]

可以看到 COPY 的部分只能知道有檔案被複製到指定目錄而已。

python 連接 redis

出處:https://cloud.tencent.com/developer/article/1892663

redis 安裝

先確保redis 已經安裝並且啟動

(host port):(docker port)

docker pull redis:latest
docker run -itd --name redis-test -p 1234:6379 redis
docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS                                       NAMES
5ccd903da485   redis     "docker-entrypoint.s…"   6 minutes ago   Up 6 minutes   0.0.0.0:1234->6379/tcp, :::1234->6379/tcp   redis-test

進入docker容器

docker exec -it redis-test /bin/bash

進入容器後,可以使用redis-cli 命令redis-cli SET key value的值,redis-cli GET key取出對應的值

root@ec62efc510ce:/data# redis-cli SET yoyo "hello world"
OK
root@ec62efc510ce:/data# redis-cli GET yoyo              
"hello world"

經過簡單的測試,說明沒有問題

python 連 reids

接著講下如何用 python 程式碼連上 redis 資料庫服務器。 先使用pip 安裝redis 驅動包

pip install redis

程式碼很簡單

import redis
r = redis.StrictRedis(host='127.0.0.1', port=1234)
print(r.get('yoyo'))

運行結果是byte類型:b'hello world',可以加個參數decode_responses=True,設定得到str字串

import redis
r = redis.StrictRedis(host='127.0.0.1', port=1234, decode_responses=True)
print(r.get('yoyo'))

於是可以得到字串:yoyo

測試下set新增鍵值對,get取值,中文也是沒問題的

import redis
r = redis.StrictRedis(host='127.0.0.1', port=1234, decode_responses=True)

# set 設定key-value
r.set("name", "上海-悠悠")
print(r.get("name"))

運行結果:上海-悠悠

當key不存在的時候,get()取值返回結果是None

Python Docker 容器與 Redis Docker 容器 連動

docker redis 安裝

docker pull redis:latest

由於您的 Python Docker 容器與 Redis Docker 容器不在同一個 Docker 網路上,因此您需要將 Python Docker 容器加入相同的 Docker 網路。您可以使用以下命令創建一個名為 my-network 的 Docker 網路:

docker network create my-network

然後,您可以使用以下命令運行 Python Docker 容器,並將其加入 my-network 網路:

docker run -itd --name redis-test -p 1234:6379 --network my-network <image_name>  使用 docker images 查看

docker run -itd --name redis-test -p 1234:6379 --network my-network redis

Build the python docker image:

  • requirements.txt
requests
kafka-python
grpcio
protobuf
better-exceptions
loguru
pandas
python-binance
redis
aiohttp
flask
kubernetes
  • Dockerfile
FROM python:3.11-slim

# Add requirements file in the container
COPY requirements.txt ./requirements.txt
RUN pip install -r requirements.txt

# Add source code in the container
COPY redis_test_script.py ./redis_test_script.py

# Define container entry point (could also work with CMD python main.py)
ENTRYPOINT ["python", "redis_test_script.py"]

在 Python 應用程式中使用 Redis 容器的 IP 位置,而不是使用 127.0.0.1。由於 Redis 容器的名稱為 redis-test,因此您可以使用以下程式碼來連接 Redis 服務:

  • redis_test_script.py
import redis
r = redis.StrictRedis(host='redis-test', port=6379, decode_responses=True)

# set 設定key-value
r.set("name", "上海-悠悠")
print(r.get("name"))

在這個命令中,-t 參數指定映像檔的名稱

docker build -t python-redis .
docker run -d --network=my-network --name my-db redis ...
docker run    --network=my-network python-redis ...

第一個指令 docker run -d --network=my-network --name my-db redis ... 是用來運行 Redis Docker 容器。這個指令中的 --network=my-network 參數表示將容器連接到 Docker 網路 my-network 中,因此其他 Docker 容器可以通過這個網路訪問 Redis 容器。 --name my-db 參數為容器指定了一個名稱 my-db,方便後續使用。redis 表示我們要使用的 Docker 映像檔名稱和版本。最後的 ... 表示可以在這個命令後面加上其他的參數來運行 Redis 容器。

第二個指令 docker run --network=my-network python-redis ... 是用來運行 Python Docker 容器。這個指令中的 --network=my-network 參數表示將容器連接到 Docker 網路 my-network 中,這樣 Python 容器就可以通過這個網路訪問 Redis 容器。 python-redis 表示我們要使用的 Docker 映像檔名稱和版本。最後的 ... 表示可以在這個命令後面加上其他的參數來運行 Python 容器。

在這兩個指令中,--network 參數用於指定容器所使用的 Docker 網路,讓不同的容器可以在同一個網路中進行通信。 --name 參數用於指定容器的名稱,方便後續使用。而最後的 ... 可以用於指定其他的參數,例如指定容器運行的命令等等。

Docker connect to host

docker build -t python-redis .

docker run --network=host -it  python-redis

Docker 容器內部的 Python 程式直接連接到宿主機器上運行的 Redis 服務。請注意,使用 --network=host 參數可能會降低容器的安全性,因為容器可以訪問宿主機器上的所有網路資源。因此,建議在必要時才使用這個參數。

import redis

r = redis.StrictRedis(host='127.0.0.1', port=6379, decode_responses=True)
# set 設定key-value
r.set("name", "上海-悠悠 127.0.0.1")
print(r.get("name"))

Docker 常用的指令

image

如果要搜索 image 可以上 dockerhub 找(裡面都有可以直接抓下來的指令)

docker pull image_name
# 從 Dockerhub 拉回 image
docker images
docker image ls
# 列出本地 image
docker rmi (image_name or image_id)
#移除 image

Container

# 查看本地 container
# -a:顯示所有執行中的 container。
docker ps (-a)
docker container ls (-a)
# 執行 container
docker start (container_id or container_name)
# 離開 container
exit  (退出並停止容器)
# 停止 container
docker stop (container_id or container_name)
# 移除 container
docker rm (container_id or container_name)

volume

將本機的某個位置掛載至 Container 的某個位置。

# 列出本地 volume
docker volume ls
# 移除 volume
docker volume rm volume_name

# 移除所有未使用的 volume
docker volume prune

清理Docker的container,image與volume

出處: https://note.qidong.name/2017/06/26/docker-clean/

Docker的鏡像(image)、容器(container)、資料卷(volume), 都是由daemon託管的。 因此,在需要清理時,也需要使用其自帶的手段。

清理技巧

清理所有停止運行的容器:

docker container prune
# or
docker rm $(docker ps -aq)

清理所有懸掛(<none>)鏡像:

docker image prune
# or
docker rmi $(docker images -qf "dangling=true")

清理所有無用資料卷:

docker volume prune

由於prune操作是批次刪除類的危險操作,所以會有一次確認。 如果不想輸入y<CR>來確認,可以新增-f操作。慎用!

清理停止的容器

docker rm -lv CONTAINER

-l是清理link,v是清理volume。 這裡的CONTAINER是容器的name或ID,可以是一個或多個。

參數列表:

Name, shorthandDefaultDescription
–force, -ffalseForce the removal of a running container (uses SIGKILL)
–link, -lfalseRemove the specified link
–volumes, -vfalseRemove the volumes associated with the container

清理所有停止的容器

通過docker ps可以查詢當前運行的容器資訊。 而通過docker ps -a,可以查詢所有的容器資訊,包括已停止的。

在需要清理所有已停止的容器時,通常利用shell的特性,組合一下就好。

docker rm $(docker ps -aq)

其中,ps-q,是隻輸出容器ID,方便作為參數讓rm使用。 假如給rm指定-f,則可以清理所有容器,包括正在運行的。

這條組合命令,等價於另一條命令:

docker container prune

container子命令,下面包含了所有和容器相關的子命令。 包括docker ps,等價於docker container psdocker container ls。 其餘還有startstopkillcp等,一級子命令相當於二級子命令在外面的alias。 而prune則是特別提供的清理命令,這在其它的管理命令裡還可以看到,比如imagevolume

按需批次清理容器

清除所有已停止的容器,是比較常用的清理。 但有時會需要做一些特殊過濾。

這時就需要使用docker ps --filter

比如,顯示所有返回值為0,即正常退出的容器:

docker ps -a --filter 'exited=0'

同理,可以得到其它非正常退出的容器。

目前支援的過濾器有:

  • id (container’s id)
  • label (label=<key> or label=<key>=<value>)
  • name (container’s name)
  • exited (int - the code of exited containers. Only useful with –all)
  • status (created|restarting|running|removing|paused|exited|dead)
  • ancestor (<image-name>[:<tag>], <image id> or <image@digest>) - filters containers that were created from the given image or a descendant.
  • before (container’s id or name) - filters containers created before given id or name
  • since (container’s id or name) - filters containers created since given id or name
  • isolation (default|process|hyperv) (Windows daemon only)
  • volume (volume name or mount point) - filters containers that mount volumes.
  • network (network id or name) - filters containers connected to the provided network
  • health (starting|healthy|unhealthy|none) - filters containers based on healthcheck status

清理失敗

如果在清理容器時發生失敗,通過重啟Docker的Daemon,應該都能解決問題。

# systemd
sudo systemctl restart docker.service

# initd
sudo service docker restart

清理鏡像

與清理容器的psrm類似,清理鏡像也有imagesrmi兩個子命令。 images用來查看,rmi用來刪除。

清理鏡像前,應該確保該鏡像的容器,已經被清除。

docker rmi IMAGE

其中,IMAGE可以是name或ID。 如果是name,不加TAG可以刪除所有TAG。

另外,這兩個命令也都屬於alias。 docker images等價於docker image ls,而docker rmi等價於docker image rm

按需批次清理鏡像

ps類似,images也支援--filter參數。

與清理相關,最常用的,當屬<none>了。

docker images --filter "dangling=true"

這條命令,可以列出所有懸掛(dangling)的鏡像,也就是顯示為<none>的那些。

docker rmi $(docker images -qf "dangling=true")

這條組合命令,如果不寫入Bash的alias,幾乎無法使用。 不過還有一條等價命令,非常容易使用。

docker image prune

pruneimages類似,也同樣支援--filter參數。 其它的filter有:

  • dangling (boolean - true or false)
  • label (label=<key> or label=<key>=<value>)
  • before (<image-name>[:<tag>], <image id> or <image@digest>) - filter images created before given id or references
  • since (<image-name>[:<tag>], <image id> or <image@digest>) - filter images created since given id or references
  • reference (pattern of an image reference) - filter images whose reference matches the specified pattern

清理所有無用鏡像

這招要慎用,否則需要重新下載。

docker image prune -a

清理資料卷

資料卷不如容器或鏡像那樣顯眼,但佔的硬碟卻可大可小。

資料卷的相關命令,都在docker volume中了。

一般用docker volume ls來查看,用docker volume rm VOLUME來刪除一個或多個。

不過,絕大多數情況下,不需要執行這兩個命令的組合。 直接執行docker volume prune就好,即可刪除所有無用卷。

注意:這是一個危險操作!甚至可以說,這是本文中最危險的操作! 一般真正有價值的運行資料,都在資料卷中。 (當然也可能掛載到了容器外的檔案系統裡,那就沒關係。) 如果在關鍵服務停止期間,執行這個操作,很可能會丟失所有資料

從檔案系統刪除

除組態檔案以為,Docker的內容相關檔案,基本都放在/var/lib/docker/目錄下。

該目錄下有下列子目錄,基本可以猜測出用途:

  • aufs
  • containers
  • image
  • network
  • plugins
  • swarm
  • tmp
  • trust
  • volumes

一般不推薦直接操作這些目錄,除非一些極特殊情況。 操作不當,後果難料,需要慎重。

這兩個 Docker 命令的差異在於它們的目的和執行方式。

docker run -it --rm redis bash 
docker exec -it redis sh
  • docker run -it --rm redis bash 命令的目的是啟動一個 Redis 容器,然後在容器中執行一個交互式 Bash shell。這個命令會建立一個新的容器,並且在容器內啟動一個新的 Bash shell。在這個 shell 中,您可以在容器內進行任何操作,就像在一個本地的 Bash shell 中一樣。這個容器會在退出 shell 後立即被刪除,因為我們使用了 --rm 選項。
  • docker exec -it redis sh 命令的目的是在一個已經運行的 Redis 容器中執行一個新的交互式 shell。這個命令不會創建新的容器,而是在現有的 Redis 容器內運行一個新的 shell。因此,使用這個命令,您必須先啟動一個 Redis 容器。在 shell 中,您可以在容器內進行任何操作,並且這些操作將會對容器內的系統環境進行更改。該容器不會被刪除,因為我們沒有使用 --rm 選項。

簡而言之,docker run 命令用於啟動新容器,而 docker exec 命令則用於在現有容器中運行命令。

使用 Docker-Compose 啟動多個 Docker Container

出處:https://ithelp.ithome.com.tw/articles/10194183

今天要介紹有關於 Docker-Compose 的部份,之前有介紹過使用 docker run 指令就可以把 Docker Container 啟動起來,但是如果我們要啟動很多個 Docker Container 時,就需要輸入很多次 docker run 指令,另外 container 和 container 之間要做關聯的話也要記得它們之間要如何的連結(link) Container,這樣在要啟動多個 Container 的情況下,就會顯得比較麻煩。

因此就出現了 Docker-Compose,只要寫一個 docker-compose.yml,把所有要使用 Docker Image 寫上去,另外也可以把 Container 之間的關係連結(link)起來,最後只要下 docker-compose up 指令,就可以把所有的 Docker Container 執行起來,這樣就可以很快速和方便的啟動多個 container。

實作的部份主要就是要把 Docker-Compose 安裝起來,然後撰寫一個 docker-compose.yml,並且使用 docker-compose up,指令把所有的 Docker Container 啟動起來,步驟如下:

1. 安裝 Docker-Compose,指令如下

https://github.com/docker/compose/releases/

sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

# cd /usr/bin
# wget https://github.com/docker/compose/releases/download/1.18.0/docker-compose-Linux-x86_64
# mv docker-compose-Linux-x86_64 docker-compose
# chmod 755 docker-compose

2. 撰寫 docker-compose.yml 檔案如下

version: '2'
services:
  db:
     image: mysql
     environment:
        MYSQL_ROOT_PASSWORD: 123456
  admin:
     image: adminer
     ports:
       - 8080:8080

這個 docker-compose.yml 的檔案,是參考以下網站,改寫出來的 https://hub.docker.com/_/mysql/

主要的功能是要啟動 2 個 Docker Container,一個是 mysql 的 Container,另外一個是 admin 管理 mysql Web UI 的 container

MYSQL_ROOT_PASSWORD 的環境變數用來設定登入 mysql 的密碼

3. 啟動所有的 Docker Container 指令如下

$ docker-compose up -d

執行所有在 docker-compose.yml 檔案裡面設定的 Docker Image 啟動 Docker Container,另外 -d 參數代表要執行在背景的方式

4. 使用 Docker-Compose 提供的指令查看 Docker Container 的執行狀態

$ docker-compose ps

要輸入此指令之前,要先把資料夾切到和 docker-compose.yml 同一層的資料夾路徑下面,執行結果如下 https://ithelp.ithome.com.tw/upload/images/20171227/20103456PGcFzKD8zW.png

5. admin 執行的畫面如下 https://ithelp.ithome.com.tw/upload/images/20171227/20103456TxflRKpTN0.png

帳號輸入 root 和密碼輸入 123456,之後就可以登入 mysql 的管理畫面,畫面如下: https://ithelp.ithome.com.tw/upload/images/20171227/20103456lWgPQa4fXY.png

6. 如果要看執行的 log 可以使用以下的指令

$ docker-compose logs

另外 logs 後面可以加上 Container Name 畫面如下 https://ithelp.ithome.com.tw/upload/images/20171227/201034564Ljx8jccEw.png

7. 如果要停止 docker-compose 執行的所有 Container 可以使用以下的指令

$ docker-compose stop

8. 如果要刪除 docker-compose 的所有 Container 可以使用以下的指令

$ docker-compose rm

畫面如下 https://ithelp.ithome.com.tw/upload/images/20171227/20103456GIP06bKAUX.png

今天介紹的 Docker-Compose 可以很方便的讓我們在執行啟動多個 Container,其實 docker-compose.yml,還有很多的寫法沒有介紹到,因此有興趣的話可以參考官方網站的說明,網址如下: https://docs.docker.com/compose/compose-file/compose-file-v2/


docker-compse 安裝 Redis

  • Run

    docker-compose -f docker-compose.yml up -d
    
  • test.py

    import redis
    
    r = redis.StrictRedis(host='127.0.0.1', port=1234, password='12345678')
    print(r.get('yoyo'))
    
  • docker-compose.yml

    version: '3.3'
    services:
      redis:
        image: redis
        container_name: redis
        command: redis-server /usr/local/etc/redis/redis.conf
        ports:
          - "1234:6379"
        volumes:
          - ./data:/data
          - ./redis.conf:/usr/local/etc/redis/redis.conf
    

    redis.conf

    # Redis configuration file example.
    #
    # Note that in order to read the configuration file, Redis must be
    # started with the file path as first argument:
    #
    # ./redis-server /path/to/redis.conf
    
    # Note on units: when memory size is needed, it is possible to specify
    # it in the usual form of 1k 5GB 4M and so forth:
    #
    # 1k => 1000 bytes
    # 1kb => 1024 bytes
    # 1m => 1000000 bytes
    # 1mb => 1024*1024 bytes
    # 1g => 1000000000 bytes
    # 1gb => 1024*1024*1024 bytes
    #
    # units are case insensitive so 1GB 1Gb 1gB are all the same.
    
    ################################## INCLUDES ###################################
    
    # Include one or more other config files here.  This is useful if you
    # have a standard template that goes to all Redis servers but also need
    # to customize a few per-server settings.  Include files can include
    # other files, so use this wisely.
    #
    # Notice option "include" won't be rewritten by command "CONFIG REWRITE"
    # from admin or Redis Sentinel. Since Redis always uses the last processed
    # line as value of a configuration directive, you'd better put includes
    # at the beginning of this file to avoid overwriting config change at runtime.
    #
    # If instead you are interested in using includes to override configuration
    # options, it is better to use include as the last line.
    #
    # include /path/to/local.conf
    # include /path/to/other.conf
    
    ################################## MODULES #####################################
    
    # Load modules at startup. If the server is not able to load modules
    # it will abort. It is possible to use multiple loadmodule directives.
    #
    # loadmodule /path/to/my_module.so
    # loadmodule /path/to/other_module.so
    
    ################################## NETWORK #####################################
    
    # By default, if no "bind" configuration directive is specified, Redis listens
    # for connections from all the network interfaces available on the server.
    # It is possible to listen to just one or multiple selected interfaces using
    # the "bind" configuration directive, followed by one or more IP addresses.
    #
    # Examples:
    #
    # bind 192.168.1.100 10.0.0.1
    # bind 127.0.0.1 ::1
    #
    # ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the
    # internet, binding to all the interfaces is dangerous and will expose the
    # instance to everybody on the internet. So by default we uncomment the
    # following bind directive, that will force Redis to listen only into
    # the IPv4 loopback interface address (this means Redis will be able to
    # accept connections only from clients running into the same computer it
    # is running).
    #
    # IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
    # JUST COMMENT THE FOLLOWING LINE.
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # bind 127.0.0.1
    
    # Protected mode is a layer of security protection, in order to avoid that
    # Redis instances left open on the internet are accessed and exploited.
    #
    # When protected mode is on and if:
    #
    # 1) The server is not binding explicitly to a set of addresses using the
    #    "bind" directive.
    # 2) No password is configured.
    #
    # The server only accepts connections from clients connecting from the
    # IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain
    # sockets.
    #
    # By default protected mode is enabled. You should disable it only if
    # you are sure you want clients from other hosts to connect to Redis
    # even if no authentication is configured, nor a specific set of interfaces
    # are explicitly listed using the "bind" directive.
    protected-mode yes
    
    # Accept connections on the specified port, default is 6379 (IANA #815344).
    # If port 0 is specified Redis will not listen on a TCP socket.
    port 6379
    
    # TCP listen() backlog.
    #
    # In high requests-per-second environments you need an high backlog in order
    # to avoid slow clients connections issues. Note that the Linux kernel
    # will silently truncate it to the value of /proc/sys/net/core/somaxconn so
    # make sure to raise both the value of somaxconn and tcp_max_syn_backlog
    # in order to get the desired effect.
    tcp-backlog 511
    
    # Unix socket.
    #
    # Specify the path for the Unix socket that will be used to listen for
    # incoming connections. There is no default, so Redis will not listen
    # on a unix socket when not specified.
    #
    # unixsocket /tmp/redis.sock
    # unixsocketperm 700
    
    # Close the connection after a client is idle for N seconds (0 to disable)
    timeout 0
    
    # TCP keepalive.
    #
    # If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence
    # of communication. This is useful for two reasons:
    #
    # 1) Detect dead peers.
    # 2) Take the connection alive from the point of view of network
    #    equipment in the middle.
    #
    # On Linux, the specified value (in seconds) is the period used to send ACKs.
    # Note that to close the connection the double of the time is needed.
    # On other kernels the period depends on the kernel configuration.
    #
    # A reasonable value for this option is 300 seconds, which is the new
    # Redis default starting with Redis 3.2.1.
    tcp-keepalive 300
    
    ################################# GENERAL #####################################
    
    # By default Redis does not run as a daemon. Use 'yes' if you need it.
    # Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
    daemonize no
    
    # If you run Redis from upstart or systemd, Redis can interact with your
    # supervision tree. Options:
    #   supervised no      - no supervision interaction
    #   supervised upstart - signal upstart by putting Redis into SIGSTOP mode
    #   supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET
    #   supervised auto    - detect upstart or systemd method based on
    #                        UPSTART_JOB or NOTIFY_SOCKET environment variables
    # Note: these supervision methods only signal "process is ready."
    #       They do not enable continuous liveness pings back to your supervisor.
    supervised no
    
    # If a pid file is specified, Redis writes it where specified at startup
    # and removes it at exit.
    #
    # When the server runs non daemonized, no pid file is created if none is
    # specified in the configuration. When the server is daemonized, the pid file
    # is used even if not specified, defaulting to "/var/run/redis.pid".
    #
    # Creating a pid file is best effort: if Redis is not able to create it
    # nothing bad happens, the server will start and run normally.
    pidfile /var/run/redis_6379.pid
    
    # Specify the server verbosity level.
    # This can be one of:
    # debug (a lot of information, useful for development/testing)
    # verbose (many rarely useful info, but not a mess like the debug level)
    # notice (moderately verbose, what you want in production probably)
    # warning (only very important / critical messages are logged)
    loglevel notice
    
    # Specify the log file name. Also the empty string can be used to force
    # Redis to log on the standard output. Note that if you use standard
    # output for logging but daemonize, logs will be sent to /dev/null
    logfile ""
    
    # To enable logging to the system logger, just set 'syslog-enabled' to yes,
    # and optionally update the other syslog parameters to suit your needs.
    # syslog-enabled no
    
    # Specify the syslog identity.
    # syslog-ident redis
    
    # Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.
    # syslog-facility local0
    
    # Set the number of databases. The default database is DB 0, you can select
    # a different one on a per-connection basis using SELECT <dbid> where
    # dbid is a number between 0 and 'databases'-1
    databases 16
    
    # By default Redis shows an ASCII art logo only when started to log to the
    # standard output and if the standard output is a TTY. Basically this means
    # that normally a logo is displayed only in interactive sessions.
    #
    # However it is possible to force the pre-4.0 behavior and always show a
    # ASCII art logo in startup logs by setting the following option to yes.
    always-show-logo yes
    
    ################################ SNAPSHOTTING  ################################
    #
    # Save the DB on disk:
    #
    #   save <seconds> <changes>
    #
    #   Will save the DB if both the given number of seconds and the given
    #   number of write operations against the DB occurred.
    #
    #   In the example below the behaviour will be to save:
    #   after 900 sec (15 min) if at least 1 key changed
    #   after 300 sec (5 min) if at least 10 keys changed
    #   after 60 sec if at least 10000 keys changed
    #
    #   Note: you can disable saving completely by commenting out all "save" lines.
    #
    #   It is also possible to remove all the previously configured save
    #   points by adding a save directive with a single empty string argument
    #   like in the following example:
    #
    #   save ""
    
    save 900 1
    save 300 10
    save 60 10000
    
    # By default Redis will stop accepting writes if RDB snapshots are enabled
    # (at least one save point) and the latest background save failed.
    # This will make the user aware (in a hard way) that data is not persisting
    # on disk properly, otherwise chances are that no one will notice and some
    # disaster will happen.
    #
    # If the background saving process will start working again Redis will
    # automatically allow writes again.
    #
    # However if you have setup your proper monitoring of the Redis server
    # and persistence, you may want to disable this feature so that Redis will
    # continue to work as usual even if there are problems with disk,
    # permissions, and so forth.
    stop-writes-on-bgsave-error yes
    
    # Compress string objects using LZF when dump .rdb databases?
    # For default that's set to 'yes' as it's almost always a win.
    # If you want to save some CPU in the saving child set it to 'no' but
    # the dataset will likely be bigger if you have compressible values or keys.
    rdbcompression yes
    
    # Since version 5 of RDB a CRC64 checksum is placed at the end of the file.
    # This makes the format more resistant to corruption but there is a performance
    # hit to pay (around 10%) when saving and loading RDB files, so you can disable it
    # for maximum performances.
    #
    # RDB files created with checksum disabled have a checksum of zero that will
    # tell the loading code to skip the check.
    rdbchecksum yes
    
    # The filename where to dump the DB
    dbfilename dump.rdb
    
    # The working directory.
    #
    # The DB will be written inside this directory, with the filename specified
    # above using the 'dbfilename' configuration directive.
    #
    # The Append Only File will also be created inside this directory.
    #
    # Note that you must specify a directory here, not a file name.
    dir ./
    
    ################################# REPLICATION #################################
    
    # Master-Replica replication. Use replicaof to make a Redis instance a copy of
    # another Redis server. A few things to understand ASAP about Redis replication.
    #
    #   +------------------+      +---------------+
    #   |      Master      | ---> |    Replica    |
    #   | (receive writes) |      |  (exact copy) |
    #   +------------------+      +---------------+
    #
    # 1) Redis replication is asynchronous, but you can configure a master to
    #    stop accepting writes if it appears to be not connected with at least
    #    a given number of replicas.
    # 2) Redis replicas are able to perform a partial resynchronization with the
    #    master if the replication link is lost for a relatively small amount of
    #    time. You may want to configure the replication backlog size (see the next
    #    sections of this file) with a sensible value depending on your needs.
    # 3) Replication is automatic and does not need user intervention. After a
    #    network partition replicas automatically try to reconnect to masters
    #    and resynchronize with them.
    #
    # replicaof <masterip> <masterport>
    
    # If the master is password protected (using the "requirepass" configuration
    # directive below) it is possible to tell the replica to authenticate before
    # starting the replication synchronization process, otherwise the master will
    # refuse the replica request.
    #
    # masterauth <master-password>
    
    # When a replica loses its connection with the master, or when the replication
    # is still in progress, the replica can act in two different ways:
    #
    # 1) if replica-serve-stale-data is set to 'yes' (the default) the replica will
    #    still reply to client requests, possibly with out of date data, or the
    #    data set may just be empty if this is the first synchronization.
    #
    # 2) if replica-serve-stale-data is set to 'no' the replica will reply with
    #    an error "SYNC with master in progress" to all the kind of commands
    #    but to INFO, replicaOF, AUTH, PING, SHUTDOWN, REPLCONF, ROLE, CONFIG,
    #    SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, PUBSUB,
    #    COMMAND, POST, HOST: and LATENCY.
    #
    replica-serve-stale-data yes
    
    # You can configure a replica instance to accept writes or not. Writing against
    # a replica instance may be useful to store some ephemeral data (because data
    # written on a replica will be easily deleted after resync with the master) but
    # may also cause problems if clients are writing to it because of a
    # misconfiguration.
    #
    # Since Redis 2.6 by default replicas are read-only.
    #
    # Note: read only replicas are not designed to be exposed to untrusted clients
    # on the internet. It's just a protection layer against misuse of the instance.
    # Still a read only replica exports by default all the administrative commands
    # such as CONFIG, DEBUG, and so forth. To a limited extent you can improve
    # security of read only replicas using 'rename-command' to shadow all the
    # administrative / dangerous commands.
    replica-read-only yes
    
    # Replication SYNC strategy: disk or socket.
    #
    # -------------------------------------------------------
    # WARNING: DISKLESS REPLICATION IS EXPERIMENTAL CURRENTLY
    # -------------------------------------------------------
    #
    # New replicas and reconnecting replicas that are not able to continue the replication
    # process just receiving differences, need to do what is called a "full
    # synchronization". An RDB file is transmitted from the master to the replicas.
    # The transmission can happen in two different ways:
    #
    # 1) Disk-backed: The Redis master creates a new process that writes the RDB
    #                 file on disk. Later the file is transferred by the parent
    #                 process to the replicas incrementally.
    # 2) Diskless: The Redis master creates a new process that directly writes the
    #              RDB file to replica sockets, without touching the disk at all.
    #
    # With disk-backed replication, while the RDB file is generated, more replicas
    # can be queued and served with the RDB file as soon as the current child producing
    # the RDB file finishes its work. With diskless replication instead once
    # the transfer starts, new replicas arriving will be queued and a new transfer
    # will start when the current one terminates.
    #
    # When diskless replication is used, the master waits a configurable amount of
    # time (in seconds) before starting the transfer in the hope that multiple replicas
    # will arrive and the transfer can be parallelized.
    #
    # With slow disks and fast (large bandwidth) networks, diskless replication
    # works better.
    repl-diskless-sync no
    
    # When diskless replication is enabled, it is possible to configure the delay
    # the server waits in order to spawn the child that transfers the RDB via socket
    # to the replicas.
    #
    # This is important since once the transfer starts, it is not possible to serve
    # new replicas arriving, that will be queued for the next RDB transfer, so the server
    # waits a delay in order to let more replicas arrive.
    #
    # The delay is specified in seconds, and by default is 5 seconds. To disable
    # it entirely just set it to 0 seconds and the transfer will start ASAP.
    repl-diskless-sync-delay 5
    
    # Replicas send PINGs to server in a predefined interval. It's possible to change
    # this interval with the repl_ping_replica_period option. The default value is 10
    # seconds.
    #
    # repl-ping-replica-period 10
    
    # The following option sets the replication timeout for:
    #
    # 1) Bulk transfer I/O during SYNC, from the point of view of replica.
    # 2) Master timeout from the point of view of replicas (data, pings).
    # 3) Replica timeout from the point of view of masters (REPLCONF ACK pings).
    #
    # It is important to make sure that this value is greater than the value
    # specified for repl-ping-replica-period otherwise a timeout will be detected
    # every time there is low traffic between the master and the replica.
    #
    # repl-timeout 60
    
    # Disable TCP_NODELAY on the replica socket after SYNC?
    #
    # If you select "yes" Redis will use a smaller number of TCP packets and
    # less bandwidth to send data to replicas. But this can add a delay for
    # the data to appear on the replica side, up to 40 milliseconds with
    # Linux kernels using a default configuration.
    #
    # If you select "no" the delay for data to appear on the replica side will
    # be reduced but more bandwidth will be used for replication.
    #
    # By default we optimize for low latency, but in very high traffic conditions
    # or when the master and replicas are many hops away, turning this to "yes" may
    # be a good idea.
    repl-disable-tcp-nodelay no
    
    # Set the replication backlog size. The backlog is a buffer that accumulates
    # replica data when replicas are disconnected for some time, so that when a replica
    # wants to reconnect again, often a full resync is not needed, but a partial
    # resync is enough, just passing the portion of data the replica missed while
    # disconnected.
    #
    # The bigger the replication backlog, the longer the time the replica can be
    # disconnected and later be able to perform a partial resynchronization.
    #
    # The backlog is only allocated once there is at least a replica connected.
    #
    # repl-backlog-size 1mb
    
    # After a master has no longer connected replicas for some time, the backlog
    # will be freed. The following option configures the amount of seconds that
    # need to elapse, starting from the time the last replica disconnected, for
    # the backlog buffer to be freed.
    #
    # Note that replicas never free the backlog for timeout, since they may be
    # promoted to masters later, and should be able to correctly "partially
    # resynchronize" with the replicas: hence they should always accumulate backlog.
    #
    # A value of 0 means to never release the backlog.
    #
    # repl-backlog-ttl 3600
    
    # The replica priority is an integer number published by Redis in the INFO output.
    # It is used by Redis Sentinel in order to select a replica to promote into a
    # master if the master is no longer working correctly.
    #
    # A replica with a low priority number is considered better for promotion, so
    # for instance if there are three replicas with priority 10, 100, 25 Sentinel will
    # pick the one with priority 10, that is the lowest.
    #
    # However a special priority of 0 marks the replica as not able to perform the
    # role of master, so a replica with priority of 0 will never be selected by
    # Redis Sentinel for promotion.
    #
    # By default the priority is 100.
    replica-priority 100
    
    # It is possible for a master to stop accepting writes if there are less than
    # N replicas connected, having a lag less or equal than M seconds.
    #
    # The N replicas need to be in "online" state.
    #
    # The lag in seconds, that must be <= the specified value, is calculated from
    # the last ping received from the replica, that is usually sent every second.
    #
    # This option does not GUARANTEE that N replicas will accept the write, but
    # will limit the window of exposure for lost writes in case not enough replicas
    # are available, to the specified number of seconds.
    #
    # For example to require at least 3 replicas with a lag <= 10 seconds use:
    #
    # min-replicas-to-write 3
    # min-replicas-max-lag 10
    #
    # Setting one or the other to 0 disables the feature.
    #
    # By default min-replicas-to-write is set to 0 (feature disabled) and
    # min-replicas-max-lag is set to 10.
    
    # A Redis master is able to list the address and port of the attached
    # replicas in different ways. For example the "INFO replication" section
    # offers this information, which is used, among other tools, by
    # Redis Sentinel in order to discover replica instances.
    # Another place where this info is available is in the output of the
    # "ROLE" command of a master.
    #
    # The listed IP and address normally reported by a replica is obtained
    # in the following way:
    #
    #   IP: The address is auto detected by checking the peer address
    #   of the socket used by the replica to connect with the master.
    #
    #   Port: The port is communicated by the replica during the replication
    #   handshake, and is normally the port that the replica is using to
    #   listen for connections.
    #
    # However when port forwarding or Network Address Translation (NAT) is
    # used, the replica may be actually reachable via different IP and port
    # pairs. The following two options can be used by a replica in order to
    # report to its master a specific set of IP and port, so that both INFO
    # and ROLE will report those values.
    #
    # There is no need to use both the options if you need to override just
    # the port or the IP address.
    #
    # replica-announce-ip 5.5.5.5
    # replica-announce-port 1234
    
    ################################## SECURITY ###################################
    
    # Require clients to issue AUTH <PASSWORD> before processing any other
    # commands.  This might be useful in environments in which you do not trust
    # others with access to the host running redis-server.
    #
    # This should stay commented out for backward compatibility and because most
    # people do not need auth (e.g. they run their own servers).
    #
    # Warning: since Redis is pretty fast an outside user can try up to
    # 150k passwords per second against a good box. This means that you should
    # use a very strong password otherwise it will be very easy to break.
    
    requirepass 12345678
    
    # Command renaming.
    #
    # It is possible to change the name of dangerous commands in a shared
    # environment. For instance the CONFIG command may be renamed into something
    # hard to guess so that it will still be available for internal-use tools
    # but not available for general clients.
    #
    # Example:
    #
    # rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52
    #
    # It is also possible to completely kill a command by renaming it into
    # an empty string:
    #
    # rename-command CONFIG ""
    #
    # Please note that changing the name of commands that are logged into the
    # AOF file or transmitted to replicas may cause problems.
    
    ################################### CLIENTS ####################################
    
    # Set the max number of connected clients at the same time. By default
    # this limit is set to 10000 clients, however if the Redis server is not
    # able to configure the process file limit to allow for the specified limit
    # the max number of allowed clients is set to the current file limit
    # minus 32 (as Redis reserves a few file descriptors for internal uses).
    #
    # Once the limit is reached Redis will close all the new connections sending
    # an error 'max number of clients reached'.
    #
    # maxclients 10000
    
    ############################## MEMORY MANAGEMENT ################################
    
    # Set a memory usage limit to the specified amount of bytes.
    # When the memory limit is reached Redis will try to remove keys
    # according to the eviction policy selected (see maxmemory-policy).
    #
    # If Redis can't remove keys according to the policy, or if the policy is
    # set to 'noeviction', Redis will start to reply with errors to commands
    # that would use more memory, like SET, LPUSH, and so on, and will continue
    # to reply to read-only commands like GET.
    #
    # This option is usually useful when using Redis as an LRU or LFU cache, or to
    # set a hard memory limit for an instance (using the 'noeviction' policy).
    #
    # WARNING: If you have replicas attached to an instance with maxmemory on,
    # the size of the output buffers needed to feed the replicas are subtracted
    # from the used memory count, so that network problems / resyncs will
    # not trigger a loop where keys are evicted, and in turn the output
    # buffer of replicas is full with DELs of keys evicted triggering the deletion
    # of more keys, and so forth until the database is completely emptied.
    #
    # In short... if you have replicas attached it is suggested that you set a lower
    # limit for maxmemory so that there is some free RAM on the system for replica
    # output buffers (but this is not needed if the policy is 'noeviction').
    #
    # maxmemory <bytes>
    
    # MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
    # is reached. You can select among five behaviors:
    #
    # volatile-lru -> Evict using approximated LRU among the keys with an expire set.
    # allkeys-lru -> Evict any key using approximated LRU.
    # volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
    # allkeys-lfu -> Evict any key using approximated LFU.
    # volatile-random -> Remove a random key among the ones with an expire set.
    # allkeys-random -> Remove a random key, any key.
    # volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
    # noeviction -> Don't evict anything, just return an error on write operations.
    #
    # LRU means Least Recently Used
    # LFU means Least Frequently Used
    #
    # Both LRU, LFU and volatile-ttl are implemented using approximated
    # randomized algorithms.
    #
    # Note: with any of the above policies, Redis will return an error on write
    #       operations, when there are no suitable keys for eviction.
    #
    #       At the date of writing these commands are: set setnx setex append
    #       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd
    #       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby
    #       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby
    #       getset mset msetnx exec sort
    #
    # The default is:
    #
    # maxmemory-policy noeviction
    
    # LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated
    # algorithms (in order to save memory), so you can tune it for speed or
    # accuracy. For default Redis will check five keys and pick the one that was
    # used less recently, you can change the sample size using the following
    # configuration directive.
    #
    # The default of 5 produces good enough results. 10 Approximates very closely
    # true LRU but costs more CPU. 3 is faster but not very accurate.
    #
    # maxmemory-samples 5
    
    # Starting from Redis 5, by default a replica will ignore its maxmemory setting
    # (unless it is promoted to master after a failover or manually). It means
    # that the eviction of keys will be just handled by the master, sending the
    # DEL commands to the replica as keys evict in the master side.
    #
    # This behavior ensures that masters and replicas stay consistent, and is usually
    # what you want, however if your replica is writable, or you want the replica to have
    # a different memory setting, and you are sure all the writes performed to the
    # replica are idempotent, then you may change this default (but be sure to understand
    # what you are doing).
    #
    # Note that since the replica by default does not evict, it may end using more
    # memory than the one set via maxmemory (there are certain buffers that may
    # be larger on the replica, or data structures may sometimes take more memory and so
    # forth). So make sure you monitor your replicas and make sure they have enough
    # memory to never hit a real out-of-memory condition before the master hits
    # the configured maxmemory setting.
    #
    # replica-ignore-maxmemory yes
    
    ############################# LAZY FREEING ####################################
    
    # Redis has two primitives to delete keys. One is called DEL and is a blocking
    # deletion of the object. It means that the server stops processing new commands
    # in order to reclaim all the memory associated with an object in a synchronous
    # way. If the key deleted is associated with a small object, the time needed
    # in order to execute the DEL command is very small and comparable to most other
    # O(1) or O(log_N) commands in Redis. However if the key is associated with an
    # aggregated value containing millions of elements, the server can block for
    # a long time (even seconds) in order to complete the operation.
    #
    # For the above reasons Redis also offers non blocking deletion primitives
    # such as UNLINK (non blocking DEL) and the ASYNC option of FLUSHALL and
    # FLUSHDB commands, in order to reclaim memory in background. Those commands
    # are executed in constant time. Another thread will incrementally free the
    # object in the background as fast as possible.
    #
    # DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled.
    # It's up to the design of the application to understand when it is a good
    # idea to use one or the other. However the Redis server sometimes has to
    # delete keys or flush the whole database as a side effect of other operations.
    # Specifically Redis deletes objects independently of a user call in the
    # following scenarios:
    #
    # 1) On eviction, because of the maxmemory and maxmemory policy configurations,
    #    in order to make room for new data, without going over the specified
    #    memory limit.
    # 2) Because of expire: when a key with an associated time to live (see the
    #    EXPIRE command) must be deleted from memory.
    # 3) Because of a side effect of a command that stores data on a key that may
    #    already exist. For example the RENAME command may delete the old key
    #    content when it is replaced with another one. Similarly SUNIONSTORE
    #    or SORT with STORE option may delete existing keys. The SET command
    #    itself removes any old content of the specified key in order to replace
    #    it with the specified string.
    # 4) During replication, when a replica performs a full resynchronization with
    #    its master, the content of the whole database is removed in order to
    #    load the RDB file just transferred.
    #
    # In all the above cases the default is to delete objects in a blocking way,
    # like if DEL was called. However you can configure each case specifically
    # in order to instead release memory in a non-blocking way like if UNLINK
    # was called, using the following configuration directives:
    
    lazyfree-lazy-eviction no
    lazyfree-lazy-expire no
    lazyfree-lazy-server-del no
    replica-lazy-flush no
    
    ############################## APPEND ONLY MODE ###############################
    
    # By default Redis asynchronously dumps the dataset on disk. This mode is
    # good enough in many applications, but an issue with the Redis process or
    # a power outage may result into a few minutes of writes lost (depending on
    # the configured save points).
    #
    # The Append Only File is an alternative persistence mode that provides
    # much better durability. For instance using the default data fsync policy
    # (see later in the config file) Redis can lose just one second of writes in a
    # dramatic event like a server power outage, or a single write if something
    # wrong with the Redis process itself happens, but the operating system is
    # still running correctly.
    #
    # AOF and RDB persistence can be enabled at the same time without problems.
    # If the AOF is enabled on startup Redis will load the AOF, that is the file
    # with the better durability guarantees.
    #
    # Please check http://redis.io/topics/persistence for more information.
    
    appendonly yes
    
    # The name of the append only file (default: "appendonly.aof")
    
    appendfilename "appendonly.aof"
    
    # The fsync() call tells the Operating System to actually write data on disk
    # instead of waiting for more data in the output buffer. Some OS will really flush
    # data on disk, some other OS will just try to do it ASAP.
    #
    # Redis supports three different modes:
    #
    # no: don't fsync, just let the OS flush the data when it wants. Faster.
    # always: fsync after every write to the append only log. Slow, Safest.
    # everysec: fsync only one time every second. Compromise.
    #
    # The default is "everysec", as that's usually the right compromise between
    # speed and data safety. It's up to you to understand if you can relax this to
    # "no" that will let the operating system flush the output buffer when
    # it wants, for better performances (but if you can live with the idea of
    # some data loss consider the default persistence mode that's snapshotting),
    # or on the contrary, use "always" that's very slow but a bit safer than
    # everysec.
    #
    # More details please check the following article:
    # http://antirez.com/post/redis-persistence-demystified.html
    #
    # If unsure, use "everysec".
    
    # appendfsync always
    appendfsync everysec
    # appendfsync no
    
    # When the AOF fsync policy is set to always or everysec, and a background
    # saving process (a background save or AOF log background rewriting) is
    # performing a lot of I/O against the disk, in some Linux configurations
    # Redis may block too long on the fsync() call. Note that there is no fix for
    # this currently, as even performing fsync in a different thread will block
    # our synchronous write(2) call.
    #
    # In order to mitigate this problem it's possible to use the following option
    # that will prevent fsync() from being called in the main process while a
    # BGSAVE or BGREWRITEAOF is in progress.
    #
    # This means that while another child is saving, the durability of Redis is
    # the same as "appendfsync none". In practical terms, this means that it is
    # possible to lose up to 30 seconds of log in the worst scenario (with the
    # default Linux settings).
    #
    # If you have latency problems turn this to "yes". Otherwise leave it as
    # "no" that is the safest pick from the point of view of durability.
    
    no-appendfsync-on-rewrite no
    
    # Automatic rewrite of the append only file.
    # Redis is able to automatically rewrite the log file implicitly calling
    # BGREWRITEAOF when the AOF log size grows by the specified percentage.
    #
    # This is how it works: Redis remembers the size of the AOF file after the
    # latest rewrite (if no rewrite has happened since the restart, the size of
    # the AOF at startup is used).
    #
    # This base size is compared to the current size. If the current size is
    # bigger than the specified percentage, the rewrite is triggered. Also
    # you need to specify a minimal size for the AOF file to be rewritten, this
    # is useful to avoid rewriting the AOF file even if the percentage increase
    # is reached but it is still pretty small.
    #
    # Specify a percentage of zero in order to disable the automatic AOF
    # rewrite feature.
    
    auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb
    
    # An AOF file may be found to be truncated at the end during the Redis
    # startup process, when the AOF data gets loaded back into memory.
    # This may happen when the system where Redis is running
    # crashes, especially when an ext4 filesystem is mounted without the
    # data=ordered option (however this can't happen when Redis itself
    # crashes or aborts but the operating system still works correctly).
    #
    # Redis can either exit with an error when this happens, or load as much
    # data as possible (the default now) and start if the AOF file is found
    # to be truncated at the end. The following option controls this behavior.
    #
    # If aof-load-truncated is set to yes, a truncated AOF file is loaded and
    # the Redis server starts emitting a log to inform the user of the event.
    # Otherwise if the option is set to no, the server aborts with an error
    # and refuses to start. When the option is set to no, the user requires
    # to fix the AOF file using the "redis-check-aof" utility before to restart
    # the server.
    #
    # Note that if the AOF file will be found to be corrupted in the middle
    # the server will still exit with an error. This option only applies when
    # Redis will try to read more data from the AOF file but not enough bytes
    # will be found.
    aof-load-truncated yes
    
    # When rewriting the AOF file, Redis is able to use an RDB preamble in the
    # AOF file for faster rewrites and recoveries. When this option is turned
    # on the rewritten AOF file is composed of two different stanzas:
    #
    #   [RDB file][AOF tail]
    #
    # When loading Redis recognizes that the AOF file starts with the "REDIS"
    # string and loads the prefixed RDB file, and continues loading the AOF
    # tail.
    aof-use-rdb-preamble yes
    
    ################################ LUA SCRIPTING  ###############################
    
    # Max execution time of a Lua script in milliseconds.
    #
    # If the maximum execution time is reached Redis will log that a script is
    # still in execution after the maximum allowed time and will start to
    # reply to queries with an error.
    #
    # When a long running script exceeds the maximum execution time only the
    # SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be
    # used to stop a script that did not yet called write commands. The second
    # is the only way to shut down the server in the case a write command was
    # already issued by the script but the user doesn't want to wait for the natural
    # termination of the script.
    #
    # Set it to 0 or a negative value for unlimited execution without warnings.
    lua-time-limit 5000
    
    ################################ REDIS CLUSTER  ###############################
    
    # Normal Redis instances can't be part of a Redis Cluster; only nodes that are
    # started as cluster nodes can. In order to start a Redis instance as a
    # cluster node enable the cluster support uncommenting the following:
    #
    # cluster-enabled yes
    
    # Every cluster node has a cluster configuration file. This file is not
    # intended to be edited by hand. It is created and updated by Redis nodes.
    # Every Redis Cluster node requires a different cluster configuration file.
    # Make sure that instances running in the same system do not have
    # overlapping cluster configuration file names.
    #
    # cluster-config-file nodes-6379.conf
    
    # Cluster node timeout is the amount of milliseconds a node must be unreachable
    # for it to be considered in failure state.
    # Most other internal time limits are multiple of the node timeout.
    #
    # cluster-node-timeout 15000
    
    # A replica of a failing master will avoid to start a failover if its data
    # looks too old.
    #
    # There is no simple way for a replica to actually have an exact measure of
    # its "data age", so the following two checks are performed:
    #
    # 1) If there are multiple replicas able to failover, they exchange messages
    #    in order to try to give an advantage to the replica with the best
    #    replication offset (more data from the master processed).
    #    Replicas will try to get their rank by offset, and apply to the start
    #    of the failover a delay proportional to their rank.
    #
    # 2) Every single replica computes the time of the last interaction with
    #    its master. This can be the last ping or command received (if the master
    #    is still in the "connected" state), or the time that elapsed since the
    #    disconnection with the master (if the replication link is currently down).
    #    If the last interaction is too old, the replica will not try to failover
    #    at all.
    #
    # The point "2" can be tuned by user. Specifically a replica will not perform
    # the failover if, since the last interaction with the master, the time
    # elapsed is greater than:
    #
    #   (node-timeout * replica-validity-factor) + repl-ping-replica-period
    #
    # So for example if node-timeout is 30 seconds, and the replica-validity-factor
    # is 10, and assuming a default repl-ping-replica-period of 10 seconds, the
    # replica will not try to failover if it was not able to talk with the master
    # for longer than 310 seconds.
    #
    # A large replica-validity-factor may allow replicas with too old data to failover
    # a master, while a too small value may prevent the cluster from being able to
    # elect a replica at all.
    #
    # For maximum availability, it is possible to set the replica-validity-factor
    # to a value of 0, which means, that replicas will always try to failover the
    # master regardless of the last time they interacted with the master.
    # (However they'll always try to apply a delay proportional to their
    # offset rank).
    #
    # Zero is the only value able to guarantee that when all the partitions heal
    # the cluster will always be able to continue.
    #
    # cluster-replica-validity-factor 10
    
    # Cluster replicas are able to migrate to orphaned masters, that are masters
    # that are left without working replicas. This improves the cluster ability
    # to resist to failures as otherwise an orphaned master can't be failed over
    # in case of failure if it has no working replicas.
    #
    # Replicas migrate to orphaned masters only if there are still at least a
    # given number of other working replicas for their old master. This number
    # is the "migration barrier". A migration barrier of 1 means that a replica
    # will migrate only if there is at least 1 other working replica for its master
    # and so forth. It usually reflects the number of replicas you want for every
    # master in your cluster.
    #
    # Default is 1 (replicas migrate only if their masters remain with at least
    # one replica). To disable migration just set it to a very large value.
    # A value of 0 can be set but is useful only for debugging and dangerous
    # in production.
    #
    # cluster-migration-barrier 1
    
    # By default Redis Cluster nodes stop accepting queries if they detect there
    # is at least an hash slot uncovered (no available node is serving it).
    # This way if the cluster is partially down (for example a range of hash slots
    # are no longer covered) all the cluster becomes, eventually, unavailable.
    # It automatically returns available as soon as all the slots are covered again.
    #
    # However sometimes you want the subset of the cluster which is working,
    # to continue to accept queries for the part of the key space that is still
    # covered. In order to do so, just set the cluster-require-full-coverage
    # option to no.
    #
    # cluster-require-full-coverage yes
    
    # This option, when set to yes, prevents replicas from trying to failover its
    # master during master failures. However the master can still perform a
    # manual failover, if forced to do so.
    #
    # This is useful in different scenarios, especially in the case of multiple
    # data center operations, where we want one side to never be promoted if not
    # in the case of a total DC failure.
    #
    # cluster-replica-no-failover no
    
    # In order to setup your cluster make sure to read the documentation
    # available at http://redis.io web site.
    
    ########################## CLUSTER DOCKER/NAT support  ########################
    
    # In certain deployments, Redis Cluster nodes address discovery fails, because
    # addresses are NAT-ted or because ports are forwarded (the typical case is
    # Docker and other containers).
    #
    # In order to make Redis Cluster working in such environments, a static
    # configuration where each node knows its public address is needed. The
    # following two options are used for this scope, and are:
    #
    # * cluster-announce-ip
    # * cluster-announce-port
    # * cluster-announce-bus-port
    #
    # Each instruct the node about its address, client port, and cluster message
    # bus port. The information is then published in the header of the bus packets
    # so that other nodes will be able to correctly map the address of the node
    # publishing the information.
    #
    # If the above options are not used, the normal Redis Cluster auto-detection
    # will be used instead.
    #
    # Note that when remapped, the bus port may not be at the fixed offset of
    # clients port + 10000, so you can specify any port and bus-port depending
    # on how they get remapped. If the bus-port is not set, a fixed offset of
    # 10000 will be used as usually.
    #
    # Example:
    #
    # cluster-announce-ip 10.1.1.5
    # cluster-announce-port 6379
    # cluster-announce-bus-port 6380
    
    ################################## SLOW LOG ###################################
    
    # The Redis Slow Log is a system to log queries that exceeded a specified
    # execution time. The execution time does not include the I/O operations
    # like talking with the client, sending the reply and so forth,
    # but just the time needed to actually execute the command (this is the only
    # stage of command execution where the thread is blocked and can not serve
    # other requests in the meantime).
    #
    # You can configure the slow log with two parameters: one tells Redis
    # what is the execution time, in microseconds, to exceed in order for the
    # command to get logged, and the other parameter is the length of the
    # slow log. When a new command is logged the oldest one is removed from the
    # queue of logged commands.
    
    # The following time is expressed in microseconds, so 1000000 is equivalent
    # to one second. Note that a negative number disables the slow log, while
    # a value of zero forces the logging of every command.
    slowlog-log-slower-than 10000
    
    # There is no limit to this length. Just be aware that it will consume memory.
    # You can reclaim memory used by the slow log with SLOWLOG RESET.
    slowlog-max-len 128
    
    ################################ LATENCY MONITOR ##############################
    
    # The Redis latency monitoring subsystem samples different operations
    # at runtime in order to collect data related to possible sources of
    # latency of a Redis instance.
    #
    # Via the LATENCY command this information is available to the user that can
    # print graphs and obtain reports.
    #
    # The system only logs operations that were performed in a time equal or
    # greater than the amount of milliseconds specified via the
    # latency-monitor-threshold configuration directive. When its value is set
    # to zero, the latency monitor is turned off.
    #
    # By default latency monitoring is disabled since it is mostly not needed
    # if you don't have latency issues, and collecting data has a performance
    # impact, that while very small, can be measured under big load. Latency
    # monitoring can easily be enabled at runtime using the command
    # "CONFIG SET latency-monitor-threshold <milliseconds>" if needed.
    latency-monitor-threshold 0
    
    ############################# EVENT NOTIFICATION ##############################
    
    # Redis can notify Pub/Sub clients about events happening in the key space.
    # This feature is documented at http://redis.io/topics/notifications
    #
    # For instance if keyspace events notification is enabled, and a client
    # performs a DEL operation on key "foo" stored in the Database 0, two
    # messages will be published via Pub/Sub:
    #
    # PUBLISH __keyspace@0__:foo del
    # PUBLISH __keyevent@0__:del foo
    #
    # It is possible to select the events that Redis will notify among a set
    # of classes. Every class is identified by a single character:
    #
    #  K     Keyspace events, published with __keyspace@<db>__ prefix.
    #  E     Keyevent events, published with __keyevent@<db>__ prefix.
    #  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
    #  $     String commands
    #  l     List commands
    #  s     Set commands
    #  h     Hash commands
    #  z     Sorted set commands
    #  x     Expired events (events generated every time a key expires)
    #  e     Evicted events (events generated when a key is evicted for maxmemory)
    #  A     Alias for g$lshzxe, so that the "AKE" string means all the events.
    #
    #  The "notify-keyspace-events" takes as argument a string that is composed
    #  of zero or multiple characters. The empty string means that notifications
    #  are disabled.
    #
    #  Example: to enable list and generic events, from the point of view of the
    #           event name, use:
    #
    #  notify-keyspace-events Elg
    #
    #  Example 2: to get the stream of the expired keys subscribing to channel
    #             name __keyevent@0__:expired use:
    #
    #  notify-keyspace-events Ex
    #
    #  By default all notifications are disabled because most users don't need
    #  this feature and the feature has some overhead. Note that if you don't
    #  specify at least one of K or E, no events will be delivered.
    notify-keyspace-events ""
    
    ############################### ADVANCED CONFIG ###############################
    
    # Hashes are encoded using a memory efficient data structure when they have a
    # small number of entries, and the biggest entry does not exceed a given
    # threshold. These thresholds can be configured using the following directives.
    hash-max-ziplist-entries 512
    hash-max-ziplist-value 64
    
    # Lists are also encoded in a special way to save a lot of space.
    # The number of entries allowed per internal list node can be specified
    # as a fixed maximum size or a maximum number of elements.
    # For a fixed maximum size, use -5 through -1, meaning:
    # -5: max size: 64 Kb  <-- not recommended for normal workloads
    # -4: max size: 32 Kb  <-- not recommended
    # -3: max size: 16 Kb  <-- probably not recommended
    # -2: max size: 8 Kb   <-- good
    # -1: max size: 4 Kb   <-- good
    # Positive numbers mean store up to _exactly_ that number of elements
    # per list node.
    # The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
    # but if your use case is unique, adjust the settings as necessary.
    list-max-ziplist-size -2
    
    # Lists may also be compressed.
    # Compress depth is the number of quicklist ziplist nodes from *each* side of
    # the list to *exclude* from compression.  The head and tail of the list
    # are always uncompressed for fast push/pop operations.  Settings are:
    # 0: disable all list compression
    # 1: depth 1 means "don't start compressing until after 1 node into the list,
    #    going from either the head or tail"
    #    So: [head]->node->node->...->node->[tail]
    #    [head], [tail] will always be uncompressed; inner nodes will compress.
    # 2: [head]->[next]->node->node->...->node->[prev]->[tail]
    #    2 here means: don't compress head or head->next or tail->prev or tail,
    #    but compress all nodes between them.
    # 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]
    # etc.
    list-compress-depth 0
    
    # Sets have a special encoding in just one case: when a set is composed
    # of just strings that happen to be integers in radix 10 in the range
    # of 64 bit signed integers.
    # The following configuration setting sets the limit in the size of the
    # set in order to use this special memory saving encoding.
    set-max-intset-entries 512
    
    # Similarly to hashes and lists, sorted sets are also specially encoded in
    # order to save a lot of space. This encoding is only used when the length and
    # elements of a sorted set are below the following limits:
    zset-max-ziplist-entries 128
    zset-max-ziplist-value 64
    
    # HyperLogLog sparse representation bytes limit. The limit includes the
    # 16 bytes header. When an HyperLogLog using the sparse representation crosses
    # this limit, it is converted into the dense representation.
    #
    # A value greater than 16000 is totally useless, since at that point the
    # dense representation is more memory efficient.
    #
    # The suggested value is ~ 3000 in order to have the benefits of
    # the space efficient encoding without slowing down too much PFADD,
    # which is O(N) with the sparse encoding. The value can be raised to
    # ~ 10000 when CPU is not a concern, but space is, and the data set is
    # composed of many HyperLogLogs with cardinality in the 0 - 15000 range.
    hll-sparse-max-bytes 3000
    
    # Streams macro node max size / items. The stream data structure is a radix
    # tree of big nodes that encode multiple items inside. Using this configuration
    # it is possible to configure how big a single node can be in bytes, and the
    # maximum number of items it may contain before switching to a new node when
    # appending new stream entries. If any of the following settings are set to
    # zero, the limit is ignored, so for instance it is possible to set just a
    # max entires limit by setting max-bytes to 0 and max-entries to the desired
    # value.
    stream-node-max-bytes 4096
    stream-node-max-entries 100
    
    # Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in
    # order to help rehashing the main Redis hash table (the one mapping top-level
    # keys to values). The hash table implementation Redis uses (see dict.c)
    # performs a lazy rehashing: the more operation you run into a hash table
    # that is rehashing, the more rehashing "steps" are performed, so if the
    # server is idle the rehashing is never complete and some more memory is used
    # by the hash table.
    #
    # The default is to use this millisecond 10 times every second in order to
    # actively rehash the main dictionaries, freeing memory when possible.
    #
    # If unsure:
    # use "activerehashing no" if you have hard latency requirements and it is
    # not a good thing in your environment that Redis can reply from time to time
    # to queries with 2 milliseconds delay.
    #
    # use "activerehashing yes" if you don't have such hard requirements but
    # want to free memory asap when possible.
    activerehashing yes
    
    # The client output buffer limits can be used to force disconnection of clients
    # that are not reading data from the server fast enough for some reason (a
    # common reason is that a Pub/Sub client can't consume messages as fast as the
    # publisher can produce them).
    #
    # The limit can be set differently for the three different classes of clients:
    #
    # normal -> normal clients including MONITOR clients
    # replica  -> replica clients
    # pubsub -> clients subscribed to at least one pubsub channel or pattern
    #
    # The syntax of every client-output-buffer-limit directive is the following:
    #
    # client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
    #
    # A client is immediately disconnected once the hard limit is reached, or if
    # the soft limit is reached and remains reached for the specified number of
    # seconds (continuously).
    # So for instance if the hard limit is 32 megabytes and the soft limit is
    # 16 megabytes / 10 seconds, the client will get disconnected immediately
    # if the size of the output buffers reach 32 megabytes, but will also get
    # disconnected if the client reaches 16 megabytes and continuously overcomes
    # the limit for 10 seconds.
    #
    # By default normal clients are not limited because they don't receive data
    # without asking (in a push way), but just after a request, so only
    # asynchronous clients may create a scenario where data is requested faster
    # than it can read.
    #
    # Instead there is a default limit for pubsub and replica clients, since
    # subscribers and replicas receive data in a push fashion.
    #
    # Both the hard or the soft limit can be disabled by setting them to zero.
    client-output-buffer-limit normal 0 0 0
    client-output-buffer-limit replica 256mb 64mb 60
    client-output-buffer-limit pubsub 32mb 8mb 60
    
    # Client query buffers accumulate new commands. They are limited to a fixed
    # amount by default in order to avoid that a protocol desynchronization (for
    # instance due to a bug in the client) will lead to unbound memory usage in
    # the query buffer. However you can configure it here if you have very special
    # needs, such us huge multi/exec requests or alike.
    #
    # client-query-buffer-limit 1gb
    
    # In the Redis protocol, bulk requests, that are, elements representing single
    # strings, are normally limited ot 512 mb. However you can change this limit
    # here.
    #
    # proto-max-bulk-len 512mb
    
    # Redis calls an internal function to perform many background tasks, like
    # closing connections of clients in timeout, purging expired keys that are
    # never requested, and so forth.
    #
    # Not all tasks are performed with the same frequency, but Redis checks for
    # tasks to perform according to the specified "hz" value.
    #
    # By default "hz" is set to 10. Raising the value will use more CPU when
    # Redis is idle, but at the same time will make Redis more responsive when
    # there are many keys expiring at the same time, and timeouts may be
    # handled with more precision.
    #
    # The range is between 1 and 500, however a value over 100 is usually not
    # a good idea. Most users should use the default of 10 and raise this up to
    # 100 only in environments where very low latency is required.
    hz 10
    
    # Normally it is useful to have an HZ value which is proportional to the
    # number of clients connected. This is useful in order, for instance, to
    # avoid too many clients are processed for each background task invocation
    # in order to avoid latency spikes.
    #
    # Since the default HZ value by default is conservatively set to 10, Redis
    # offers, and enables by default, the ability to use an adaptive HZ value
    # which will temporary raise when there are many connected clients.
    #
    # When dynamic HZ is enabled, the actual configured HZ will be used as
    # as a baseline, but multiples of the configured HZ value will be actually
    # used as needed once more clients are connected. In this way an idle
    # instance will use very little CPU time while a busy instance will be
    # more responsive.
    dynamic-hz yes
    
    # When a child rewrites the AOF file, if the following option is enabled
    # the file will be fsync-ed every 32 MB of data generated. This is useful
    # in order to commit the file to the disk more incrementally and avoid
    # big latency spikes.
    aof-rewrite-incremental-fsync yes
    
    # When redis saves RDB file, if the following option is enabled
    # the file will be fsync-ed every 32 MB of data generated. This is useful
    # in order to commit the file to the disk more incrementally and avoid
    # big latency spikes.
    rdb-save-incremental-fsync yes
    
    # Redis LFU eviction (see maxmemory setting) can be tuned. However it is a good
    # idea to start with the default settings and only change them after investigating
    # how to improve the performances and how the keys LFU change over time, which
    # is possible to inspect via the OBJECT FREQ command.
    #
    # There are two tunable parameters in the Redis LFU implementation: the
    # counter logarithm factor and the counter decay time. It is important to
    # understand what the two parameters mean before changing them.
    #
    # The LFU counter is just 8 bits per key, it's maximum value is 255, so Redis
    # uses a probabilistic increment with logarithmic behavior. Given the value
    # of the old counter, when a key is accessed, the counter is incremented in
    # this way:
    #
    # 1. A random number R between 0 and 1 is extracted.
    # 2. A probability P is calculated as 1/(old_value*lfu_log_factor+1).
    # 3. The counter is incremented only if R < P.
    #
    # The default lfu-log-factor is 10. This is a table of how the frequency
    # counter changes with a different number of accesses with different
    # logarithmic factors:
    #
    # +--------+------------+------------+------------+------------+------------+
    # | factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |
    # +--------+------------+------------+------------+------------+------------+
    # | 0      | 104        | 255        | 255        | 255        | 255        |
    # +--------+------------+------------+------------+------------+------------+
    # | 1      | 18         | 49         | 255        | 255        | 255        |
    # +--------+------------+------------+------------+------------+------------+
    # | 10     | 10         | 18         | 142        | 255        | 255        |
    # +--------+------------+------------+------------+------------+------------+
    # | 100    | 8          | 11         | 49         | 143        | 255        |
    # +--------+------------+------------+------------+------------+------------+
    #
    # NOTE: The above table was obtained by running the following commands:
    #
    #   redis-benchmark -n 1000000 incr foo
    #   redis-cli object freq foo
    #
    # NOTE 2: The counter initial value is 5 in order to give new objects a chance
    # to accumulate hits.
    #
    # The counter decay time is the time, in minutes, that must elapse in order
    # for the key counter to be divided by two (or decremented if it has a value
    # less <= 10).
    #
    # The default value for the lfu-decay-time is 1. A Special value of 0 means to
    # decay the counter every time it happens to be scanned.
    #
    # lfu-log-factor 10
    # lfu-decay-time 1
    
    ########################### ACTIVE DEFRAGMENTATION #######################
    #
    # WARNING THIS FEATURE IS EXPERIMENTAL. However it was stress tested
    # even in production and manually tested by multiple engineers for some
    # time.
    #
    # What is active defragmentation?
    # -------------------------------
    #
    # Active (online) defragmentation allows a Redis server to compact the
    # spaces left between small allocations and deallocations of data in memory,
    # thus allowing to reclaim back memory.
    #
    # Fragmentation is a natural process that happens with every allocator (but
    # less so with Jemalloc, fortunately) and certain workloads. Normally a server
    # restart is needed in order to lower the fragmentation, or at least to flush
    # away all the data and create it again. However thanks to this feature
    # implemented by Oran Agra for Redis 4.0 this process can happen at runtime
    # in an "hot" way, while the server is running.
    #
    # Basically when the fragmentation is over a certain level (see the
    # configuration options below) Redis will start to create new copies of the
    # values in contiguous memory regions by exploiting certain specific Jemalloc
    # features (in order to understand if an allocation is causing fragmentation
    # and to allocate it in a better place), and at the same time, will release the
    # old copies of the data. This process, repeated incrementally for all the keys
    # will cause the fragmentation to drop back to normal values.
    #
    # Important things to understand:
    #
    # 1. This feature is disabled by default, and only works if you compiled Redis
    #    to use the copy of Jemalloc we ship with the source code of Redis.
    #    This is the default with Linux builds.
    #
    # 2. You never need to enable this feature if you don't have fragmentation
    #    issues.
    #
    # 3. Once you experience fragmentation, you can enable this feature when
    #    needed with the command "CONFIG SET activedefrag yes".
    #
    # The configuration parameters are able to fine tune the behavior of the
    # defragmentation process. If you are not sure about what they mean it is
    # a good idea to leave the defaults untouched.
    
    # Enabled active defragmentation
    # activedefrag yes
    
    # Minimum amount of fragmentation waste to start active defrag
    # active-defrag-ignore-bytes 100mb
    
    # Minimum percentage of fragmentation to start active defrag
    # active-defrag-threshold-lower 10
    
    # Maximum percentage of fragmentation at which we use maximum effort
    # active-defrag-threshold-upper 100
    
    # Minimal effort for defrag in CPU percentage
    # active-defrag-cycle-min 5
    
    # Maximal effort for defrag in CPU percentage
    # active-defrag-cycle-max 75
    
    # Maximum number of set/hash/zset/list fields that will be processed from
    # the main dictionary scan
    # active-defrag-max-scan-fields 1000
    

Redis + Python

version: '3.9'

services:
  redis:
    image: redis
    container_name: redis
    ports:
      - 1234:6379
    restart: always

  python:
    image: python:3.11
    container_name: python
    ports:
      - 8001:80
    command: sh -c 'pip install redis && tail -f /dev/null'
    depends_on:
      - redis
    restart: always

Redis + Python + Appium + Gitlab

version: '3.9'

services:
  redis:
    image: redis
    container_name: redis
    ports:
      - 1234:6379
    restart: always
    networks:
      - my-network

  python:
    image: python:3.11
    container_name: python
    ports:
      - 8001:80
    command: sh -c 'pip install redis && tail -f /dev/null'
    depends_on:
      - redis
    restart: always
    networks:
      - my-network

  gitlab:
    image: gitlab/gitlab-ce
    container_name: gitlab
    ports:
      - 8081:80
    restart: always
    networks:
      - my-network

  appium:
    image: appium/appium
    container_name: appium
    ports:
      - 4723:4723
    restart: always
    networks:
      - my-network

networks:
  my-network:
    driver: bridge

docker compose + Dockerfile 綜合應用

出處:https://medium.com/%E7%A8%8B%E5%BC%8F%E4%B9%BE%E8%B2%A8/docker-docker-compose-dockerfile-%E7%B6%9C%E5%90%88%E6%87%89%E7%94%A8-7e71ff371ebc

先來講講使用場景吧

舉個簡單的例子:假設我需要一個可以運行 python 的環境、還有一個 redis server 的環境,我要執行 python script 在 redis 上做一些資料儲存與運算。我一開始可能會這樣子啟動、seperate-compose.yml會這樣寫:

img

兩個容器,彼此用網路連接

seperate-compose.yml

version: '3.5'
services:
  python:
    image: python:3.9-slim
    container_name: py-env
    restart: always
    ports:
      - 8001:80
    command: sh -c 'pip install redis && tail -f /dev/null'
    links:
      - redis # 連結到 redis,讓兩個 container 可以互通網路
  redis:
    image: redis:6-alpine3.15
    restart: always
    container_name: redis
    ports:
      - 127.0.0.1:6379:6379

用指令

docker compose -f seperate-compose.yml up -d

來啟動之後,就可以啟動兩個 container(py-env, redis)

img

成功啟動 py-env, redis 兩個容器

接著進去剛啟動的 py-env 容器,bash 啟動 python環境連看看 redis,成功在redis這個container中塞入一筆資料(Key: “test”, Value: 1)。來敲指令吧~

$ docker exec -it py-env sh #先進入 py-env 並啟動 shell
# python3 #在容器裡運行python
>>> import redis #載入redis套件
>>> r = redis.Redis(host='redis', port=6379) #連進容器的redis
>>> r.set("test", 1) #塞一筆資料看看
True #代表塞入成功!
>>> exit() #退出python
# exit #退出container

接著來 redis 這個容器看一下資料情形吧~一樣敲指令~

$ docker exec -it redis sh
# redis-cli #進入redis client端
127.0.0.1:6379> get test #獲取剛剛的key值: test
"1" # 成功返回Value
127.0.0.1:6379>

img

分別進入 py-env, redis 這兩個容器測試資料

BUT…我想要換個做法

這樣我每次都要啟動 py-env, redis這兩個 container 互連才能夠把服務建立起來,我能不能一次到位?只啟動一個 container 就好呢?

i.e 我想要一個 container 同時擁有 python 與 redis 的服務!

img

一個容器兩種享受,我全都要 🤤

眼尖的讀者可發現在前面的seperate-compose.yml ,其實兩個容器的基底image分別是 python:3.9-slimredis:6-alpine3.15 ,那要在哪裡找一個基底image、是在 build 的時候同時擁有 python+redis的環境呢?

答案就在一開始,我要自己寫一個 Dockerfile 定義好 image。接著再利用 docker compose 把這個強大的 image 啟動成 container💪

先來看看 Dockerfile吧

首先,用下面這個 Dockerfile 建立起 image,簡單敘述一下裡頭做了些什麼事情:

# 1. 抓取基底image: redis
FROM redis:6-alpine3.15 

# 2. 基於redis image, 開始安裝 python 相關環境與套件
RUN apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python
RUN python3 -m ensurepip
RUN pip3 install --no-cache --upgrade pip setuptools
RUN pip install --no-cache-dir redis

再來就是 docker compose 的 yml 檔

重頭戲來撰寫 combine-compose.yml

# docker compose -f redis-compose.yml up -d
version: "3"

services:
  redis:
    build:
      dockerfile: Dockerfile #基於Dockerfile建立image
    restart: always
    container_name: redis #容器名稱
    ports:
      - 6379:6379
    command: sh -c "redis-server --daemonize yes && tail -f /dev/null" #啟動 redis-server

接著執行 docker compose -f combine-compose.yml up -d 來啟動 container

img

只剩下一個 redis 容器,還擁有 python 環境喔 🥳

接下來進去 redis 這個 container 玩玩看吧

  1. 進入容器並啟動shell

    $ docker exec -it redis sh
    
  2. 啟動python並嘗試在redis中塞入一筆資料,再退出python

    # python3
    >>> import redis
    >>> r = redis.Redis(host='redis', port=6379)
    >>> r.set("test_comb", 1)
    True
    >>> exit()
    
  3. 去redis-server看看資料有沒有成功塞入

    /data# redis-cli
    127.0.0.1:6379> get test_comb
    "1"
    

img

你有發現嗎?所有動作都在 redis 這個容器裡完成

如此一氣呵成~都在同一個容器內完成🥴

總結

核心概念就是先建立 image,才能啟動 container。建立 image 的方式可以用現成的 docker pull、或是自己寫 Dockerfile 建立; 啟動 container 的方式可以用一般 docker run 指令、或是本文中 docker compose 的方式一次啟動多個。只要掌握住了,萬變不離其宗😎

以上程式碼都放在 github 上了,有興趣的讀者可以抓下來玩玩看

延伸閱讀

以現今的趨勢,其實還是以啟動多容器來架構整個服務比較常見。我會有這種搞怪的做法,單純是因為我想要在 redis 啟動時可以 config 做一些特殊設定,例如:在每天的半夜12點reset鍵值、或是每隔1小時新增一個鍵值。

這些需求沒有辦法用 redis 本身提供的指令做到,所以我只能另外啟動一個 python 環境運行 python script,來做 redis config 特殊設定。

當然可能還是有redis本身支援的方式,就有勞各位大神如果有更好的做法,還請不吝告知小弟,大家互相學習增長:)


#up:啟動 Docker 組合。
#down:停止 Docker 組合。
#logs:顯示 Docker 組合的日誌。
#redis-cli:啟動 Redis 容器的 CLI。
#build:編譯映像。
REDIS_COMPOSE = combine-compose.yml
REDIS_CONTAINER = redis
IMAGE_NAME = redis-python

.PHONY: up down logs redis-cli exec build

up:
	@docker-compose -f $(REDIS_COMPOSE) up -d

down:
	@docker-compose -f $(REDIS_COMPOSE) down

logs:
	@docker-compose -f $(REDIS_COMPOSE) logs -f

redis-cli:
	@docker exec -it $(REDIS_CONTAINER) redis-cli

exec:
	@docker exec -it $(REDIS_CONTAINER) /bin/bash

build:
	docker build -t $(IMAGE_NAME) .
FROM redis

RUN apt-get update && apt-get install -y vim redis-server wget build-essential && \
    wget -q http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz && \
    tar xzf ta-lib-0.4.0-src.tar.gz && \
    cd ta-lib/ && \
    ./configure --prefix=/usr && \
    make && \
    make install && \
    cd .. && \
    rm -rf ta-lib ta-lib-0.4.0-src.tar.gz

RUN wget https://repo.anaconda.com/miniconda/Miniconda3-py38_23.1.0-1-Linux-x86_64.sh -O /tmp/miniconda.sh && \
    /bin/bash /tmp/miniconda.sh -b -p /opt/conda && \
    rm /tmp/miniconda.sh

ENV PATH=/opt/conda/bin:$PATH

COPY requirements.txt /tmp/requirements.txt
RUN pip install -r /tmp/requirements.txt && \
    rm /tmp/requirements.txt

RUN mkdir -p /usr/src/app

COPY *.py /usr/src/app

WORKDIR /usr/src/app/
# docker compose -f redis-compose.yml up -d
version: "3"

services:
  redis:
    build:
      dockerfile: Dockerfile #基於Dockerfile建立image
    image: redis-python
    restart: always
    container_name: redis #容器名稱
    ports:
      - 6380:6379
    command: sh -c "redis-server --daemonize yes && tail -f /dev/null" #啟動 redis-server
import redis 

r = redis.Redis(host="localhost", port=6379, db=1)
r.set('foo', 'bar')
print(r.get('foo'))


print ("hello world!")
print ("Welcome to python cron job")

Redash 入門

出處:https://zhuanlan.zhihu.com/p/444590189

  • 下載程式碼
git clone https://github.com/getredash/redash.git
cd redash/
git checkout v10.1.0
  • 啟動docker服務

在docker-compose.yml 的同級目錄,新建檔案 .env,內容如下:

REDASH_SECRET_KEY=隨機字串1
REDASH_COOKIE_SECRET=隨機字串2
GOOGLE_CLIENT_ID=隨機字串3

不要把這個檔案提交到git中。然後在命令列輸入:

docker-compose up -d
  • 安裝node packages
npm install -g yarn
  • 建立資料庫
# 建表
docker-compose -f docker-compose.yml run --rm redash create_db

# 建測試資料
docker-compose run --rm postgres psql -h postgres -U postgres -c "create database tests"

img

  • 常見問題

    • 使用docker-compose up -d啟動時遇到Error response from daemon: OCI runtime create failed: container_linux.go:367: starting container process caused: exec: "/app/bin/docker-entrypoint": permission denied: unknown

解決:修改宿主機上的檔案權限:

 sudo chmod 755 /bin/docker-entrypoint
 sudo chmod 755 manager.py
  • yarn --frozen-lockfile 時出現node版本不滿足。

使用nvm 管理node版本。nvm 簡單使用方法如下:

nvm list
nvm install 12.0.0
nvm use 12.0.0
node -v 
nvm uninstall 12.0.0
  • 前端報錯:Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

在執行相關命令但shell中執行如下命令

export NODE_OPTIONS="--max-old-space-size=8192"

增加記憶體:如8192,16384這樣。

  • 個人感受

程式碼比較清晰明瞭,但是每次生成可視化圖表都要徒手寫query,和superset相比工作量太大。如果習慣於點點滑鼠就生成chart,我還是推薦superset,包括二次開發,儘管superset的code有些疊床架屋的感覺。

ClickHouse 簡單部署&使用測試

出處:https://zhuanlan.zhihu.com/p/383817560

介紹

ClickHouse 是一個真正的列式資料庫管理系統(DBMS)。在 ClickHouse 中,資料始終是按列儲存的,包括向量(向量或列塊)執行的過程。只要有可能,操作都是基於向量進行分派的,而不是單個的值,這被稱為«向量化查詢執行»,它有利於降低實際的資料處理開銷。

這個想法並不新鮮,其可以追溯到 APL 程式語言及其後代:A +、J、K 和 Q。向量程式設計被大量用於科學資料處理中。即使在關係型資料庫中,這個想法也不是什麼新的東西:比如,向量程式設計也被大量用於 Vectorwise 系統中。

通常有兩種不同的加速查詢處理的方法:向量化查詢執行和執行階段程式碼生成。在後者中,動態地為每一類查詢生成程式碼,消除了間接分派和動態分派。這兩種方法中,並沒有哪一種嚴格地比另一種好。執行階段程式碼生成可以更好地將多個操作融合在一起,從而充分利用 CPU 執行單元和流水線。向量化查詢執行不是特別實用,因為它涉及必須寫到快取並讀回的臨時向量。如果 L2 快取容納不下臨時資料,那麼這將成為一個問題。但向量化查詢執行更容易利用 CPU 的 SIMD 功能。朋友寫的一篇研究論文表明,將兩種方法結合起來是更好的選擇。ClickHouse 使用了向量化查詢執行,同時初步提供了有限的執行階段動態程式碼生成。

瞭解更多詳情...

1.部署 Clickhouse 服務

建立clickhouse 工作目錄

1.1, pull 鏡像

按照官方推薦的鏡像拉取

docker pull yandex/clickhouse-server

1.2,docker-compose 啟動鏡像

version: "3"

services:
  clickhouse-server:
    image: yandex/clickhouse-server:latest
    container_name: clickhouse-server
    hostname: clickhouse-server
    networks:
      - my-network
    ports:
      - 1111:8123
    restart: always


  python-client:
    image: python:3.11
    container_name: python-client
    hostname: python-client
    command: sh -c 'apt-get update && apt-get install -y vim iputils-ping net-tools && pip install lxml loguru line-notify clickhouse_driver rel numpy pandahouse websocket-client && tail -f /dev/null'
    networks:
      - my-network
    restart: always
    volumes:
      - ./python-client:/app

networks:
  my-network:
    driver: bridge

查看運行狀態

http://127.0.0.1:1111/ 會出現OK字樣代表成功

思考:如何修改clickhouse默認組態?資料如何持久化?如何查看日誌?

1.3 更改默認組態,資料持久化,查看日誌

進入容器查看組態

docker exec -it clickhouse-server /bin/bash

clickhouse 默認組態路徑是: /etc/clickhouse-server/

退出容器,複製組態檔案當前工作目錄

docker cp 容器名:/etc/clickhouse-server/config.xml .
docker cp 容器名:/etc/clickhouse-server/uses.xml .

修改docker-compose 組態檔案

volumes: 
  # 默認組態 寫入config.d/users.d 目錄防止更新後檔案丟失
  - ./config.xml:/etc/clickhouse-server/config.d/config.xml:rw
  - ./users.xml:/etc/clickhouse-server/users.xml:rw
  # 運行日誌
  - ./logs:/var/log/clickhouse-server
  # 資料持久
  - ./data:/var/lib/clickhouse:rw

修改後的完整組態如下

docker network ls  // 如果沒 my-network 要create
docker network create my-network
version: "3"

services:
  clickhouse-server:
    image: yandex/clickhouse-server:latest
    container_name: clickhouse-server
    hostname: clickhouse-server
    networks:
      - my-network
    ports:
      - 1111:8123
      - 8888:9000
    restart: always
    volumes: 
      # 默認組態 寫入config.d/users.d 目錄防止更新後檔案丟失
      - ./config.xml:/etc/clickhouse-server/config.d/config.xml:rw
      - ./users.xml:/etc/clickhouse-server/users.xml:rw
      # 運行日誌
      - ./logs:/var/log/clickhouse-server
      # 資料持久
      - ./data:/var/lib/clickhouse:rw


  python-client:
    image: python:3.11
    container_name: python-client
    hostname: python-client
    command: sh -c 'apt-get update && apt-get install -y vim iputils-ping net-tools && pip install lxml loguru line-notify clickhouse_driver rel numpy pandahouse websocket-client && tail -f /dev/null'
    networks:
      - my-network
    volumes:
      - ./python-client:/app

networks:
  my-network:
    driver: bridge

重新啟動服務

docker-compose down && docker-compose up -d

啟動後目錄結構

├── config.xml
├── data
├── docker-compose-final.yml
├── docker-compose.yml
├── log
├── logs
└── users.xml

瀏覽器再次訪問 http://127.0.0.1:1111/ 會出現OK字樣代表成功

查看容器日誌

docker-compose logs -f

進入容器查看服務資料庫

docker exec -it clickhouse-server /bin/bash

root@clickhouse-server:/# clickhouse-client 
ClickHouse client version 22.1.3.7 (official build).
Connecting to localhost:9000 as user default.
Connected to ClickHouse server version 22.1.3 revision 54455.


clickhouse-server :) show databases;

SHOW DATABASES

Query id: aef1df36-bc09-4c33-8821-c669c8a93c2c

┌─name───────────────┐
│ INFORMATION_SCHEMA │
│ default            │
│ information_schema │
│ system             │
└────────────────────┘

4 rows in set. Elapsed: 0.001 sec. 

服務運行正常!

1.4,建立使用者登錄 (後期使用)

查看users.xml檔案

自訂使用者

<halobug>
    <password_sha256_hex>4754fc7e290a9c280d9497b2d76dd854e77f7e1c92476577fdb52ed22afc13e7</password_sha256_hex>
    <networks incl="networks" replace="replace">
        <ip>::/0</ip>
    </networks>
    <profile>default</profile>
    <quota>default</quota>
    <allow_databases>
       <database>tutorial</database>
       <database>CRYPTO</database>
    </allow_databases>
</halobug>

生成密碼(進入容器運行)

PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha256sum | tr -d '-'

FcP5O5HY
8aaa72053341d6fa19bdd150c8e1ff328e2d56444df1c977ac22f88710bac5ae 

黃色標註為自訂使用者,紅色為生成密碼

驗證使用者密碼,重新啟動服務

# 重啟
docker-compose down && docker-compose up -d
# 進入容器
docker exec -it clickhouse-server /bin/bash
# 驗證使用者名稱密碼
clickhouse-client -u halobug -h 127.0.0.1 --password FcP5O5HY  # FcP5O5HY 是出生成來的


# 建立資料庫 tutorial , tutorial 是 users.xml 建立
CREATE DATABASE tutorial;
CREATE DATABASE CRYPTO;

查看驗證結果

成功!到此服務就搭建完成!

登入 python-clinet 測試讀寫 clickhost

docker exec -it python-client /bin/bash

# 測試是否連通
ping clickhouse-server 
import clickhouse_driver
import pandas as pd


connection_settings = {
    'host': 'clickhouse-server',
    'port': '9000',
    'user': 'halobug',
    'password': 'FcP5O5HY'
}

client = clickhouse_driver.Client(**connection_settings)

# 建立新資料庫
client.execute('CREATE DATABASE IF NOT EXISTS CRYPTO')

print(client.execute("SHOW DATABASES"))
client.execute('USE CRYPTO')

# 創建一個簡單的表格
df = pd.DataFrame({'name': ['Alice', 'Bob', 'Charlie'], 'age': [25, 30, 35]})
client.execute('CREATE TABLE IF NOT EXISTS test_table (name String, age Int32) ENGINE = Memory')

# 將資料框寫入表格
client.execute('INSERT INTO test_table VALUES', df.to_dict('records'))

# 讀取表格中的資料
result = client.execute('SELECT * FROM test_table')
print(result)
from line_notify import LineNotify
from loguru import logger
from clickhouse_driver import Client
from multiprocessing import Process, Queue, Pipe
import signal
import multiprocessing
import rel
import numpy as np
import pandahouse as ph
import pandas as pd
import json
import websocket
import datetime as dt
import os
import sys
import traceback
import time


def monitor(monitor_queue, thread_name, ws_client_p, record_p):
    while True:
        try:
            data = monitor_queue.get(timeout=30)
            logger.info(f"{thread_name}, {os.getpid()}, {data[0]}")
        except Exception as e:
            logger.warning(f"{thread_name} monitor queue is empty, {e}")
            LineNotify("KXwzqEGtIp1JEkS5GjqXqRAT0D4BdQQvCNcqOa7ySfz").send(
                f"{thread_name} monitor queue is empty"
            )
            os.kill(ws_client_p.pid, signal.SIGUSR1)


def getErrMsg(e):
    error_class = e.__class__.__name__  # 取得錯誤類型
    detail = e.args[0]  # 取得詳細內容
    cl, exc, tb = sys.exc_info()  # 取得Call Stack
    lastCallStack = traceback.extract_tb(tb)[-1]  # 取得Call Stack的最後一筆資料
    fileName = lastCallStack[0]  # 取得發生的檔案名稱
    lineNum = lastCallStack[1]  # 取得發生的行號
    funcName = lastCallStack[2]  # 取得發生的函數名稱
    errMsg = 'File "{}", line {}, in {}: [{}] {}'.format(
        fileName, lineNum, funcName, error_class, detail
    )
    return errMsg


logger.add(
    f"{__file__}.log", encoding="utf-8", enqueue=True, retention="10 days",
)


class Watcher:
    def __init__(self):
        self.child = os.fork()
        if self.child == 0:
            return
        else:
            self.watch()

    def watch(self):
        try:
            os.wait()
        except KeyboardInterrupt:
            self.kill()
        sys.exit()

    def kill(self):
        try:
            print("kill")
            os.kill(self.child, signal.SIGKILL)
        except OSError:
            pass


class WebSocketClient(multiprocessing.Process):
    def __init__(
        self, thread_name, ws_url, symbol, params, queue, monitor_queue, line_notify
    ):
        multiprocessing.Process.__init__(self)
        self.__thread_name = thread_name
        self.__ws_url = ws_url
        self.__symbol = symbol
        self.__params = params
        self.__queue = queue
        self.__monitor_queue = monitor_queue
        self.__line_notify = line_notify
        self.__ws = websocket.WebSocketApp(
            self.__ws_url,
            on_open=self.on_open,
            on_message=self.on_message,
            on_close=self.on_close,
            on_error=self.on_error,
            # on_cont_message=self.on_cont_message,
            on_ping=self.on_ping,
            on_pong=self.on_pong,
        )
        self.msg_count = 0
        self.reconnecting_flag = False
        signal.signal(signal.SIGUSR1, self.receive_signal)

    def receive_signal(self, signum, stack):
        logger.warning(
            f"Received:, {signum}, {os.getpid()}, reconnecting_flag:{self.reconnecting_flag}"
        )
        # if not self.reconnecting_flag:
        #    self.reconnecting_flag = True
        #    self.reconnecting()
        # thread = threading.Thread(target=self.reconnecting, args=())
        # thread.start()

    def run(self):
        # self.__ws.run_forever()
        try:
            self.__ws.run_forever(ping_interval=0, dispatcher=rel, reconnect=3)
            rel.signal(2, rel.abort)  # Keyboard Interrupt
            rel.dispatch()
        except Exception as e:
            logger.exception(e)
            # os.kill(os.getpid(), signal.SIGUSR1)
            self.reconnecting()

    def on_open(self, ws):
        logger.info(f"on_pong : {self.reconnecting_flag}")
        self.reconnecting_flag = False
        subscribe_message = {
            "method": "SUBSCRIBE",
            "params": [self.__params],
            "id": 1,
        }
        ws.send(json.dumps(subscribe_message))

    def on_message(self, ws, message):
        if self.__queue.full():
            logger.error("queue is full")
            self.__line_notify.send("queue is full")
        else:
            msg = json.loads(message)
            self.__queue.put(msg)
            self.msg_count = self.msg_count + 1

        if self.msg_count % 20 == 0:
            self.__monitor_queue.put([dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")])
            logger.info(
                f"{self.__thread_name} msg_count:{self.msg_count} qsize:{self.__queue.qsize()}"
            )
            self.msg_count = 0

        # print(json.dumps(msg, indent=4, ensure_ascii=False))

    def on_close(self, ws, status_code, message):
        logger.warning(f"on_close {status_code} {message}")
        self.__line_notify.send("on_close " + str(message))
        self.reconnecting()

    def on_error(self, ws, error):
        logger.error(str(error))
        self.__line_notify.send("on_error " + str(error))
        # self.reconnecting()

    def on_cont_message(self, ws, message, flag):
        logger.warning(f"on_cont_message: {message} {flag}")

    def on_ping(self, ws, message):
        logger.info(f"{self.__thread_name} on_ping")
        ws.send(message, websocket.ABNF.OPCODE_PONG)

    def on_pong(self, ws, message):
        logger.info(f"{self.__thread_name} on_pong")
        ws.send(message, websocket.ABNF.OPCODE_PONG)

    def reconnecting(self):
        logger.info(f"{self.__thread_name} {os.uname()[1]} reconnecting")
        # self.__ws.close()
        # thread = threading.Thread(target=self.__ws.close, args=())
        # thread.start()
        # thread.join()

        # self.__ws.my_close()
        # rel.abort()
        # logger.info(f"{self.__thread_name} websocket closed")

        rel.abort()
        self.__ws.run_forever(ping_interval=0, dispatcher=rel, reconnect=3)
        rel.signal(2, rel.abort)
        rel.dispatch()


class Record(multiprocessing.Process):
    def __init__(self, thread_name, ws_client_p, queue, line_notify):
        multiprocessing.Process.__init__(self)
        self.__thread_name = thread_name
        self.__ws_client_p = ws_client_p
        self.__queue = queue
        self.__line_notify = line_notify
        self.__data = []

    def clear_data(self):
        self.__data.clear()

    def create_db_table(self, df, str_database, str_table):
        dtypes_dict = dict(df.dtypes)
        ch_type_convert_dict = {
            np.dtype("datetime64[ns]"): "Datetime64",
            np.dtype("int64"): "Int64",
            np.dtype("float64"): "Float64",
            np.dtype("object"): "String",
            np.dtype("bool"): "Bool",
        }
        create_table_cmd_str = ""
        for x in dtypes_dict:
            type_str = ch_type_convert_dict.get(dtypes_dict[x], None)
            if type_str is None:
                print(f"Undefined type {dtypes_dict[x]}")
            create_table_cmd_str = (
                create_table_cmd_str + f"`{x}` {type_str} DEFAULT 0, "
            )
        # create_table_cmd_str = f"CREATE TABLE IF NOT EXISTS {str_database}.{str_table} ( {create_table_cmd_str[:-2]}) ENGINE = Log"
        create_table_cmd_str = f"CREATE TABLE IF NOT EXISTS {str_database}.{str_table} ({create_table_cmd_str[:-2]}) ENGINE = MergeTree PARTITION BY year_month_day ORDER BY year_month_day SETTINGS index_granularity = 16384"
        client = Client(host="clickhouse-server", port="9000", user="halobug", password="FcP5O5HY")
        client.execute(f"CREATE DATABASE IF NOT EXISTS {str_database};")
        client.execute(create_table_cmd_str)

    def write_orderbook_to_db(self):
        df = None
        try:
            if "e" in self.__data[0].keys() and self.__data[0]["e"] == "depthUpdate":
                df = pd.DataFrame(self.__data)
                if not df.isnull().values.any():
                    df.rename(
                        columns={"E": "timestamp", "a": "asks", "b": "bids"},
                        inplace=True,
                    )
                    df["date"] = [
                        dt.datetime.fromtimestamp(x / 1000.0) for x in df["timestamp"]
                    ]
                    df["year_month_day"] = [
                        dt.datetime.fromtimestamp(x / 1000.0).strftime("%Y-%m-%d")
                        for x in df["timestamp"]
                    ]
                    df.rename(columns={"s": "pair"}, inplace=True)
                    # 僅保留需要的列
                    df = df[["pair", "asks", "bids", "date", "year_month_day"]]

                    db_name = "CRYPTO"
                    table_name = "BinanceOrderbookPartition_simplifiedFields"
                    connection_info = dict(
                        database=db_name,
                        host="http://clickhouse-server:8123/",
                        user="halobug",
                        password="FcP5O5HY",
                    )

                    self.create_db_table(df, db_name, table_name)
                    ph.to_clickhouse(
                        df,
                        table_name,
                        index=False,
                        chunksize=100000,
                        connection=connection_info,
                    )
                print(df)

            if (
                "event" in self.__data[0].keys()
                and self.__data[0]["event"] == "ORDER_BOOK"
            ):
                df = pd.DataFrame(self.__data)
                if not df.isnull().values.any():
                    df["date"] = [
                        dt.datetime.fromtimestamp(x / 1000.0).strftime(
                            "%Y-%m-%d %H:%M:%S.%f"
                        )
                        for x in df["timestamp"]
                    ]
                    df["year_month_day"] = [
                        dt.datetime.fromtimestamp(x / 1000.0).strftime("%Y-%m-%d")
                        for x in df["timestamp"]
                    ]

                    df = df[["pair", "asks", "bids", "date", "year_month_day"]]
                    db_name = "CRYPTO"
                    table_name = "BitoProOrderbookPartition_simplifiedFields"
                    connection_info = dict(
                        database=db_name,
                        host="http://clickhouse-server:8123/",
                        user="halobug",
                        password="FcP5O5HY",
                    )

                    self.create_db_table(df, db_name, table_name)
                    ph.to_clickhouse(
                        df,
                        table_name,
                        index=False,
                        chunksize=100000,
                        connection=connection_info,
                    )
                print(df)
            self.__data.clear()
        except Exception as e:
            logger.error(df)
            self.__data.clear()
            self.__line_notify.send(getErrMsg(e))
            logger.exception(e)
            os.kill(self.__ws_client_p.pid, signal.SIGUSR1)

    def run(self):
        while True:
            try:
                msg = self.__queue.get(block=True)
                self.__data.append(msg)
                if len(self.__data) > 100:
                    self.write_orderbook_to_db()
            except Exception as e:
                self.__line_notify.send(getErrMsg(e))
                logger.exception(e)


class Manager(multiprocessing.Process):
    def __init__(self, thread_name, ws_url, symbol, params, queue, line_token):
        # threading.Thread.__init__(self)
        multiprocessing.Process.__init__(self)
        self.__thread_name = thread_name
        self.__ws_url = ws_url
        self.__symbol = symbol
        self.__params = params
        self.__queue = queue
        self.__line_notify = LineNotify(line_token)

    def run(self):
        self.__line_notify.send(f"thread_name:{self.__thread_name}")
        monitor_queue = Queue(100)
        ws_client_p = WebSocketClient(
            thread_name=self.__thread_name,
            ws_url=self.__ws_url,
            symbol=self.__symbol,
            params=self.__params,
            queue=self.__queue,
            monitor_queue=monitor_queue,
            line_notify=self.__line_notify,
        )
        record_p = Record(
            thread_name=self.__thread_name,
            ws_client_p=ws_client_p,
            queue=self.__queue,
            line_notify=self.__line_notify,
        )
        tasks = [
            ws_client_p,
            record_p,
        ]
        for t in tasks:
            t.start()

        monitor_task = Process(
            target=monitor,
            args=(monitor_queue, self.__thread_name, ws_client_p, record_p),
        )
        monitor_task.start()

        for task in tasks:
            task.join()


if __name__ == "__main__":
    Watcher()
    bitopro_orderbook_queue = Queue(1000)
    binance_orderbook_queue = Queue(1000)
    tasks = [
        Manager(
            thread_name="BitoPro Thread",
            ws_url="wss://stream.bitopro.com:9443/ws/v1/pub/order-books/BTC_USDT:20",
            symbol="",
            params="",
            queue=bitopro_orderbook_queue,
            line_token="KXwzqEGtIp1JEkS5GjqXqRAT0D4BdQQvCNcqOa7ySfz",
        ),
        Manager(
            thread_name="Binance Thread",
            ws_url="wss://stream.binance.com:9443/ws",
            symbol="",
            params="btcusdt@depth@100ms",
            queue=binance_orderbook_queue,
            line_token="KXwzqEGtIp1JEkS5GjqXqRAT0D4BdQQvCNcqOa7ySfz",
        ),
    ]
    for t in tasks:
        t.start()

    for task in tasks:
        task.join()

dbeaver 登入 docker clickhouse

port 1111
halobug
FcP5O5HY

Tutorial for set up clickhouse server

https://github.com/jneo8/clickhouse-setup

  • Makefile
run-single-server:
	docker run -d --name clickhouse-server -p 9911:9000 -p 1121:8123 --ulimit nofile=262144:262144 yandex/clickhouse-server

run-single-client:
	docker run -it --rm --link clickhouse-server:clickhouse-server yandex/clickhouse-client  --host clickhouse-server

run-cluster-client-1:
	docker run -it --rm --network="clickhouse-net" --link clickhouse-01:clickhouse-server yandex/clickhouse-client --host clickhouse-server
run-cluster-client-2:
	docker run -it --rm --network="clickhouse-net" --link clickhouse-02:clickhouse-server yandex/clickhouse-client --host clickhouse-server
run-cluster-client-3:
	docker run -it --rm --network="clickhouse-net" --link clickhouse-03:clickhouse-server yandex/clickhouse-client --host clickhouse-server
run-cluster-client-4:
	docker run -it --rm --network="clickhouse-net" --link clickhouse-04:clickhouse-server yandex/clickhouse-client --host clickhouse-server
run-cluster-client-5:
	docker run -it --rm --network="clickhouse-net" --link clickhouse-05:clickhouse-server yandex/clickhouse-client --host clickhouse-server
run-cluster-client-6:
	docker run -it --rm --network="clickhouse-net" --link clickhouse-06:clickhouse-server yandex/clickhouse-client --host clickhouse-server

run-cluster-client-1-auth:
	docker run -it --rm --network="clickhouse-net" --link clickhouse-01:clickhouse-server yandex/clickhouse-client --host clickhouse-server -u user1 --password 123456

exec:
	docker exec -it clickhouse-server /bin/bash
  • docker-compose.yml
version: '3'

services:
    clickhouse-zookeeper:
        image: zookeeper
        ports:
            - "2181:2181"
            - "2182:2182"
        container_name: clickhouse-zookeeper
        hostname: clickhouse-zookeeper

    clickhouse-01:
        image: yandex/clickhouse-server
        hostname: clickhouse-01
        container_name: clickhouse-01
        ports:
            - 9001:9000
        volumes:
                - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml
                - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml
                - ./config/macros/macros-01.xml:/etc/clickhouse-server/config.d/macros.xml
                - ./config/users.xml:/etc/clickhouse-server/users.xml
                # - ./data/server-01:/var/lib/clickhouse
        ulimits:
            nofile:
                soft: 262144
                hard: 262144
        depends_on:
            - "clickhouse-zookeeper"

    clickhouse-02:
        image: yandex/clickhouse-server
        hostname: clickhouse-02
        container_name: clickhouse-02
        ports:
            - 9002:9000
        volumes:
                - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml
                - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml
                - ./config/macros/macros-02.xml:/etc/clickhouse-server/config.d/macros.xml
                - ./config/users.xml:/etc/clickhouse-server/users.xml
                # - ./data/server-02:/var/lib/clickhouse
        ulimits:
            nofile:
                soft: 262144
                hard: 262144
        depends_on:
            - "clickhouse-zookeeper"

    clickhouse-03:
        image: yandex/clickhouse-server
        hostname: clickhouse-03
        container_name: clickhouse-03
        ports:
            - 9003:9000
        volumes:
                - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml
                - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml
                - ./config/macros/macros-03.xml:/etc/clickhouse-server/config.d/macros.xml
                - ./config/users.xml:/etc/clickhouse-server/users.xml
                # - ./data/server-03:/var/lib/clickhouse
        ulimits:
            nofile:
                soft: 262144
                hard: 262144
        depends_on:
            - "clickhouse-zookeeper"

    clickhouse-04:
        image: yandex/clickhouse-server
        hostname: clickhouse-04
        container_name: clickhouse-04
        ports:
            - 9004:9000
        volumes:
                - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml
                - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml
                - ./config/macros/macros-04.xml:/etc/clickhouse-server/config.d/macros.xml
                - ./config/users.xml:/etc/clickhouse-server/users.xml
                # - ./data/server-04:/var/lib/clickhouse
        ulimits:
            nofile:
                soft: 262144
                hard: 262144
        depends_on:
            - "clickhouse-zookeeper"

    clickhouse-05:
        image: yandex/clickhouse-server
        hostname: clickhouse-05
        container_name: clickhouse-05
        ports:
            - 9005:9000
        volumes:
                - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml
                - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml
                - ./config/macros/macros-05.xml:/etc/clickhouse-server/config.d/macros.xml
                - ./config/users.xml:/etc/clickhouse-server/users.xml
                # - ./data/server-05:/var/lib/clickhouse
        ulimits:
            nofile:
                soft: 262144
                hard: 262144
        depends_on:
            - "clickhouse-zookeeper"

    clickhouse-06:
        image: yandex/clickhouse-server
        hostname: clickhouse-06
        container_name: clickhouse-06
        ports:
            - 9006:9000
        volumes:
                - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml
                - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml
                - ./config/macros/macros-06.xml:/etc/clickhouse-server/config.d/macros.xml
                - ./config/users.xml:/etc/clickhouse-server/users.xml
                # - ./data/server-06:/var/lib/clickhouse
        ulimits:
            nofile:
                soft: 262144
                hard: 262144
        depends_on:
            - "clickhouse-zookeeper"
networks:
    default:
        external:
            name: clickhouse-net

Host 建立資料庫

import clickhouse_driver
import pandas as pd


connection_settings = {
    'host': 'localhost',
    'port': '9911',
}

client = clickhouse_driver.Client(**connection_settings)

# 建立新資料庫
client.execute('CREATE DATABASE IF NOT EXISTS CRYPTO')

print(client.execute("SHOW DATABASES"))
client.execute('USE CRYPTO')

# 創建一個簡單的表格
df = pd.DataFrame({'name': ['Alice', 'Bob', 'Charlie']*3333, 'age': [25, 30, 35]*3333})
#df = pd.DataFrame({'name': ['Alice', 'Bob', 'Charlie'], 'age': [25, 30, 35]})

client.execute('CREATE TABLE IF NOT EXISTS test_table (name String, age Int32) ENGINE = Memory')
#client.execute('CREATE TABLE IF NOT EXISTS test_table (name String, age Int32) ENGINE = MergeTree PARTITION BY name ORDER BY age')


# 將資料框寫入表格
client.execute('INSERT INTO test_table VALUES', df.to_dict('records'))

# 讀取表格中的資料
result = client.execute('SELECT * FROM test_table')
print(result)

# 刪除表格
# client.execute('DROP TABLE IF EXISTS test_table')

Docker 安裝 python 運行 local python script

# 使用官方的Python 3.10鏡像作為基礎鏡像
FROM python:3.10

# 將當前目錄下的所有文件複製到容器的/app目錄
COPY . /app

# 設置工作目錄為/app
WORKDIR /app
docker build -t my-python-app .


docker run -v /本地文件的絕對路徑:/app my-python-app python /app/my_script.py
docker run -v /tmp/test_docker:/app my-python-app python my_script.py 
# my_script.py 
print('hello')

使用Docker部署Redpanda

docker pull vectorized/redpanda
docker run -d --name redpanda-node -p 9092:9092 -p 9644:9644 vectorized/redpanda

https://github.com/redpanda-data/redpanda/

from confluent_kafka import Producer, Consumer

producer = Producer({"bootstrap.servers": "localhost:9092"})
producer.produce("test-topic", key="key", value="hello redpanda!")
producer.flush()


consumer = Consumer(
    {
        "bootstrap.servers": "localhost:9092",
        "group.id": "test-group",
        "auto.offset.reset": "earliest",
    }
)
consumer.subscribe(["test-topic"])


while True:
    msg = consumer.poll(1.0)

    if msg is None:
        continue

    if msg.error():
        print(f"Consumer error: {msg.error()}")
        continue

    print(f"Received message: {msg.value().decode('utf-8')}")

consumer.close()

使用 Docker 運行最新版本 GCC 的完整指南

方法一:使用官方 GCC Docker 映像檔(推薦)

1. 建立 Dockerfile

# 使用最新版本的 GCC 官方映像檔
FROM gcc:15

# 設置工作目錄
WORKDIR /app

# 更新套件管理器並安裝必要工具
RUN apt-get update && apt-get install -y \
    vim \
    nano \
    gdb \
    make \
    cmake \
    && rm -rf /var/lib/apt/lists/*

# 建立程式碼目錄
RUN mkdir -p /app/src

# 設置環境變數
ENV CC=gcc
ENV CXX=g++

# 預設命令
CMD ["bash"]

2. 建立測試程式

建立一個簡單的 C++ 程式來測試:

hello.cpp

#include <iostream>
#include <vector>
#include <string>

int main() {
    std::vector<std::string> messages = {
        "Hello from GCC 15!",
        "C++17 is the default standard",
        "Docker + GCC = 🚀"
    };
    
    for (const auto& msg : messages) {
        std::cout << msg << std::endl;
    }
    
    // 顯示 GCC 版本
    std::cout << "\nCompiled with GCC version: " << __VERSION__ << std::endl;
    
    return 0;
}

3. 建立並運行容器

# 建立 Docker 映像檔
docker build -t gcc-dev .

# 運行容器(掛載當前目錄到容器內)
docker run -it -v $(pwd):/app/src gcc-dev

# 或者在 Windows PowerShell 中
docker run -it -v ${PWD}:/app/src gcc-dev

4. 在容器內編譯和運行

# 進入容器後,切換到源碼目錄
cd /app/src

# 編譯 C++ 程式
g++ -std=c++17 -O2 -Wall -Wextra hello.cpp -o hello

# 運行程式
./hello

# 檢查 GCC 版本
gcc --version
g++ --version

方法二:從源碼編譯 GCC 15(進階用法)

1. 進階 Dockerfile

# 使用 Ubuntu 作為基礎映像檔
FROM ubuntu:22.04

# 設置非互動模式
ENV DEBIAN_FRONTEND=noninteractive

# 安裝建構依賴
RUN apt-get update && apt-get install -y \
    build-essential \
    wget \
    curl \
    tar \
    xz-utils \
    libgmp-dev \
    libmpfr-dev \
    libmpc-dev \
    libisl-dev \
    zlib1g-dev \
    && rm -rf /var/lib/apt/lists/*

# 設置工作目錄
WORKDIR /tmp

# 下載並編譯 GCC 15.1
RUN wget https://ftp.gnu.org/gnu/gcc/gcc-15.1.0/gcc-15.1.0.tar.xz \
    && tar -xf gcc-15.1.0.tar.xz \
    && cd gcc-15.1.0 \
    && ./contrib/download_prerequisites \
    && mkdir build \
    && cd build \
    && ../configure \
        --prefix=/usr/local/gcc-15 \
        --enable-languages=c,c++,fortran \
        --disable-multilib \
        --with-system-zlib \
    && make -j$(nproc) \
    && make install \
    && cd / \
    && rm -rf /tmp/gcc-15.1.0*

# 設置環境變數
ENV PATH="/usr/local/gcc-15/bin:${PATH}"
ENV LD_LIBRARY_PATH="/usr/local/gcc-15/lib64:${LD_LIBRARY_PATH}"

# 建立工作目錄
WORKDIR /app

# 預設命令
CMD ["bash"]

方法三:使用 docker-compose(推薦用於開發)

1. 建立 docker-compose.yml

version: '3.8'

services:
  gcc-dev:
    build: .
    container_name: gcc-dev-container
    volumes:
      - .:/app/src
      - gcc-cache:/root/.cache
    working_dir: /app/src
    tty: true
    stdin_open: true
    environment:
      - CC=gcc
      - CXX=g++
      - MAKEFLAGS=-j$(nproc)

volumes:
  gcc-cache:

2. 使用 docker-compose 運行

# 建立並啟動容器
docker-compose up -d

# 進入容器
docker-compose exec gcc-dev bash

# 停止容器
docker-compose down

常用編譯指令

基本編譯

# 編譯 C 程式
gcc -std=c17 -O2 -Wall -Wextra program.c -o program

# 編譯 C++ 程式
g++ -std=c++17 -O2 -Wall -Wextra program.cpp -o program

# 編譯 C++23 程式(實驗性功能)
g++ -std=c++23 -O2 -Wall -Wextra program.cpp -o program

除錯編譯

# 包含除錯資訊
g++ -std=c++17 -g -Wall -Wextra program.cpp -o program

# 使用 GDB 除錯
gdb ./program

效能優化編譯

# 高度優化
g++ -std=c++17 -O3 -march=native -Wall -Wextra program.cpp -o program

# 連結時優化
g++ -std=c++17 -O3 -flto -Wall -Wextra program.cpp -o program

建議的專案結構

my-cpp-project/
├── Dockerfile
├── docker-compose.yml
├── src/
│   ├── main.cpp
│   ├── utils.cpp
│   └── utils.h
├── tests/
│   └── test_main.cpp
├── Makefile
└── README.md

範例 Makefile

CXX = g++
CXXFLAGS = -std=c++17 -O2 -Wall -Wextra
TARGET = app
SRCDIR = src
SOURCES = $(wildcard $(SRCDIR)/*.cpp)
OBJECTS = $(SOURCES:.cpp=.o)

$(TARGET): $(OBJECTS)
	$(CXX) $(OBJECTS) -o $(TARGET)

%.o: %.cpp
	$(CXX) $(CXXFLAGS) -c $< -o $@

clean:
	rm -f $(OBJECTS) $(TARGET)

.PHONY: clean

快速開始腳本

建立一個 start.sh 腳本:

#!/bin/bash

# 建立並運行 GCC 開發環境
echo "正在建立 GCC 開發環境..."
docker build -t gcc-dev .

echo "正在啟動容器..."
docker run -it -v $(pwd):/app/src gcc-dev

echo "環境已準備完成!"

使用方法:

chmod +x start.sh
./start.sh

這樣你就可以在 Docker 容器中使用最新版本的 GCC 15.1 來編譯和運行 C/C++ 程式了!

安裝 Rust

Linux or Unix or MacOS

輸入下面的指令即可

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

若安裝完後發生問題,例如環境變數沒有自行設定好,可以輸入下面的指令

source $HOME/.cargo/env

或在 ~/.bash_profile 輸入

export PATH="$HOME/.cargo/bin:$PATH"

Windows

下載 rustup.exe

安裝需要用到 C++ build tools for VS2013 或更高的版本,可以在 Visual Studio 的官網下載軟體後,在其他的安裝選項中找到

更新 Rust 版本

rustup update

安裝 Rust 的其他版本

EX: 安裝 nightly 的版本

rustup install nightly

輸入下面指令檢查版本,如果有顯示就是代表安裝成功

rustc --version
rustc 1.67.0-nightly (e0098a5cc 2022-11-29)

將 nightly 版本作為 default 版本

rustup default nightly

切換rust版本

查看目前所以安裝的版本

ls ~/.rustup/toolchains/

nightly-x86_64-unknown-linux-gnu  
stable-x86_64-unknown-linux-gnu

切換預設版本

rustup default stable-x86_64-unknown-linux-gnu

解除安裝 Rust 與 rustup

rustup self uninstall

Rust 版本的差異

  • nightly: 每天的最新版本,但 bug 很多
  • beta: nightly 的新 bug feature 過一段時間穩定後,會在 beta 版出現
  • stable: 最穩定的版本,但相對的功能較舊

Rustup 與 Cargo 角色介紹

Rustup

Rustup 是 Rust 程式語言的官方工具鏈管理器,主要負責:

  • 安裝與管理不同版本的 Rust 編譯器
  • 切換發布渠道(stable、beta、nightly)
  • 管理交叉編譯目標平台
  • 安裝或移除 Rust 組件

Rustup 常用指令:

  • rustup install <toolchain> - 安裝特定版本的 Rust
  • rustup update - 更新所有已安裝的工具鏈
  • rustup default <toolchain> - 設定默認工具鏈
  • rustup show - 顯示目前工具鏈資訊
  • rustup component add <component> - 安裝組件
  • rustup component remove <component> - 移除組件
  • rustup target add <target> - 增加交叉編譯目標
  • rustup target remove <target> - 移除交叉編譯目標
  • rustup self update - 更新 rustup 工具本身
  • rustup toolchain list - 列出已安裝的工具鏈

Cargo

Cargo 是 Rust 的套件管理器與建構系統,主要負責:

  • 創建新的 Rust 專案
  • 編譯與執行 Rust 專案
  • 管理專案依賴關係
  • 執行測試及基準測試
  • 發布套件到 crates.io(Rust 的官方套件儲存庫)

Cargo 常用指令:

  • cargo new <project> - 創建新專案
  • cargo init - 在現有目錄中初始化 Rust 專案
  • cargo build - 編譯專案
  • cargo run - 編譯並執行專案
  • cargo test - 執行測試
  • cargo bench - 執行基準測試
  • cargo add <crate> - 添加依賴
  • cargo remove <crate> - 移除依賴
  • cargo update - 更新依賴
  • cargo check - 檢查代碼但不生成執行檔
  • cargo doc - 生成文檔
  • cargo publish - 發布套件到 crates.io
  • cargo search <keyword> - 搜尋 crates.io 上的套件
  • cargo clean - 清除編譯生成的檔案
  • cargo fmt - 使用 rustfmt 格式化代碼
  • cargo clippy - 使用 clippy 進行更嚴格的代碼檢查

兩者關係

Rustup 與 Cargo 的關係:

  • Rustup 管理 Rust 語言本身的安裝、版本與組件
  • Cargo 管理 Rust 專案及其依賴

當您安裝 Rustup 時,它會自動為您安裝 Cargo。這兩個工具協同工作,提供完整的 Rust 開發環境,從語言工具鏈的安裝與管理,到專案的創建、編譯、測試與發布。

參考資料

  • https://www.rust-lang.org/tools/install
  • https://doc.rust-lang.org/book/ch01-01-installation.html

學習網站

Rust 筆記

  • as_ptr()
fn print_type_of<T>(_: T) {
    println!("{}", std::any::type_name::<T>());
}

fn main() {
    let free_coloring_book = vec![
        "mercury", "venus", "earth", "mars", "jupiter", "saturn", "uranus", "neptune",
    ];
    // 1. free_coloring_book堆上數據的地址
    println!("address: {:p}", free_coloring_book.as_ptr());

    // 2. free_coloring_book棧上的地址
    let a = &free_coloring_book;
    println!("address: {:p}", a);

    let mut friends_coloring_book = free_coloring_book;

    // 3. friends_coloring_book堆上數據的地址,和1一樣
    println!("address: {:p}", friends_coloring_book.as_ptr());

    // 4. friends_coloring_book棧上的地址
    let b = &friends_coloring_book;
    println!("address: {:p}", b);
}
  • Rc / Box 用法
use std::rc::Rc;

struct Aa {
    id: i32,
}

impl Drop for Aa {
    fn drop(&mut self) {
        println!("Aa Drop, id: {}", self.id);
    }
}

fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>());
}

fn test_1() {
    let a1 = Aa { id: 1 }; // 數據分配在棧中
    let a1 = Rc::new(a1); // 數據 move 到了堆中?
    print_type_of(&a1);
    //drop(a1);
    println!("xxxxxxx");
}

fn test_2() {
    let a1 = Aa { id: 1 }; // 數據分配在棧中
    let a1 = Box::new(a1); // 數據 move 到了堆中?
    print_type_of(&a1);
}

fn main() {
    test_1();
    test_2();
}
  • data bss text heap stack
/// .Text段存放的是程序中的可執行代碼
/// .Data段保存的是已經初始化了的全局變量和靜態變量
/// .ROData(ReadOnlyData)段存放程序中的常量值,如字符串常量
/// .BSS段存放的是未初始化的全局變量和靜態變量,程序執行前會先進行一遍初始化
const G_ARRAY: [i32; 5] = [10; 5];
const G_X: i32 = 100;
static G_VAR: i32 = 1000;

fn test06_heap_or_stack() {
    let s: &str = "test list";
    //字符串字面量,位於ROData段
    println!("&str: {:p}", s); //&str: 0x7ff77e4c6b88
    println!("{:p}", &G_ARRAY); //data段:0x7ff6c5fc6bb8
    println!("{:p}", &G_X); //data段:0x7ff6c5fc64f0
    println!("{:p}", &G_VAR); //data段:0x7ff77e4c6200
    println!("{}", "-".repeat(10));

    // 位於堆
    let bi = Box::new(30);
    println!("{:p}", bi); //堆:0x19f6c6585e0
                          // 將字符串字面量從內存中的代碼區(ROData段)復制一份到堆
                          // 棧上分配的變量s1指向堆內存
    let mut s1: String = String::from("Hello");
    // 可以通過std::mem::transmute將
    // 從24字節的長度的3個uszie讀出來
    let pstr: [usize; 3] = unsafe { std::mem::transmute(s1) };
    // pstr[0]是一個堆內存地址
    println!("ptr: 0x{:x}", pstr[0]); //ptr: 0x19f6c658750

    println!("{}", "-".repeat(10));
    // 位於棧
    let nums1 = [1, 2, 3, 4, 5, 6];
    let mut list: Vec<i32> = vec![20, 30, 40];
    let t = 100;
    println!("{:p}", &t); //棧0x116aeff104
    println!("{:p}", &nums1); //棧0x116aeff0d0
    println!("{:p}", &list); //棧0x116aeff0e8
                             // 從ROData區復制了一份字符串字面量放到堆上,
                             // 然後用棧上分配的s指向堆
    let s: String = "Hello".to_owned();
    println!("{:p}", &s); //0x116aeff1f8
    let s: String = String::from("Hello");
    println!("{:p}", &s); //0x116aeff260
    let s: String = "Hello".into();
    println!("{:p}", &s); //0x116aeff2c8
}

fn main() {
    test06_heap_or_stack();
}

ownership有個特性是個大坑

// ownership有個特性,感覺是個大坑,把不可變資料的ownership move到可變資料,那麼就改值了。這個設定不安全。
fn main() {
    let immutable = Box::new(5u32);
    println!("{:}", immutable);

    let mut mutable_box = immutable;
    println!("{:}", mutable_box);
    *mutable_box = 4;
    println!("{:}", mutable_box);
} 

Rust中mut, &, &mut的區別

資源:記憶體區塊。不同的記憶體區塊位置和大小就是不同的資源。

str

let a = "xxx".to_string();   含義:a繫結到字串資源A上,擁有資源A的所有權

let mut a = "xxx".to_string();  含義:a繫結到字串資源A上,擁有資源A的所有權,同時a還可繫結到新的資源上面去(更新繫結的能力,但新舊資源類型要同);

value

let b = a; 含義:a繫結的資源A轉移給b,b擁有這個資源A

let b = &a;   含義:a繫結的資源A借給b使用,b只有資源A的讀權限

let b = &mut a;   含義:a繫結的資源A借給b使用,b有資源A的讀寫權限

let mut b = &mut a;   含義:a繫結的資源A借給b使用,b有資源A的讀寫權限。同時,b可繫結到新的資源上面去(更新繫結的能力)

String

fn do(c: String) {}   含義:傳參的時候,實參d繫結的資源D的所有權轉移給c

fn do(c: &String) {}   含義:傳參的時候,實參d將繫結的資源D借給c使用,c對資源D唯讀

fn do(c: &mut String) {}   含義:傳參的時候,實參d將繫結的資源D借給c使用,c對資源D可讀寫

fn do(mut c: &mut String) {}   含義:傳參的時候,實參d將繫結的資源D借給c使用,c對資源D可讀寫。同時,c可繫結到新的資源上面去(更新繫結的能力)

函數參數裡面,冒號左邊的部分,mut c,這個mut是對函數體內部有效;冒號右邊的部分,&mut String,這個 &mut 是針對外部實參傳入時的形式化(類型)說明。

下面的例子輸出是什麼:

fn concat_literal(s: &mut String) {     
    s.extend("world!".chars());         
}                                       
                                          
fn main() {                             
    let mut s = "hello, ".to_owned();   
    concat_literal(&mut s);             
    println!("{}", s);                  
}  

打印 borrow 位址

fn print_type_of<T>(_: T) {
    println!("{}", std::any::type_name::<T>());
}

fn test(a: &mut i32) {
    println!("{:p}", *&a); // 打印 reference address 
    println!("{:p}", a); // 打印 reference address 
    println!("{:}", a);
    *a = 100;
    println!("{:p}", &a);
}

fn main() {
    let mut a = 10;
    print_type_of(a);
    let b = &mut a;
    *b = 50;
    print_type_of(b);
    println!("{:p}", &a);
    test(&mut a);
    println!("{:}", a);
}
fn print_type_of<T>(_: T) {
    println!("{}", std::any::type_name::<T>());
}

fn main() {
    let a: i32 = 5;
    print_type_of(a);
    println!("addr:{:p}", &a);
    let b = &a;
    print_type_of(b);
    println!("addr:{:p}", b);
    println!("value :{:}", b);
    //&a先轉成raw指針,然後再把指針轉成usize,這個可以print的
    let addr = &a as *const i32 as usize;
    println!("addr:0x{:X}", addr);

    //為了驗證剛才的地址是不是正確的,我們修改這個指針指向的數據
    //pa就是addr對應的raw指針
    let pa = addr as *mut i32;
    //解引用,*pa其實就是&mut a了,給他賦值100
    unsafe { *pa = 100 };

    //打印a,可以看到a已經變成100了
    println!("value:{}", a);
}

&self 和 self 的區別

在 Rust 的方法中,第一個參數為 & self,那麼如果改成 self(不是大寫的 Self)行不行,兩者有什麼區別。 &self,表示向函數傳遞的是一個引用,不會發生對像所有權的轉移; self,表示向函數傳遞的是一個對象,會發生所有權的轉移,對象的所有權會傳遞到函數中。 原文作者:linghuyichong 轉自鏈接:https://learnku.com/articles/39050

#[derive(Debug)]
struct MyType {
    name: String,
}

impl MyType {
    fn do_something(self, age: u32) {
        //等價於 fn do_something(self: Self, age: u32) {
        //等價於 fn do_something(self: MyType, age: u32) {
        println!("name = {}", self.name);
        println!("age = {}", age);
    }

    fn do_something2(&self, age: u32) {
        println!("name = {}", self.name);
        println!("age = {}", age);
    }
}

fn main() {
    let my_type = MyType {
        name: "linghuyichong".to_string(),
    };
    //使用self
    my_type.do_something(18); //等價於MyType::do_something(my_type, 18);
                              //println!("my_type: {:#?}", my_type);    //在do_something中,傳入的是對象,而不是引用,因此my_type的所有權就轉移到函數中了,因此不能再使用

    //使用&self
    let my_type2 = MyType {
        name: "linghuyichong".to_string(),
    };
    my_type2.do_something2(18);
    my_type2.do_something2(18);
    println!("my_type2: {:#?}", my_type2); //在do_something中,傳入是引用,函數並沒有獲取my_type2的所有權,因此此處可以使用
    println!("Hello, world!");
}
  • 模擬C++ 建構/解構

    use std::thread;
    use std::process;
    
    struct MyStruct {
        value: i32,
    }
    
    impl MyStruct {
        fn new(value: i32) -> MyStruct {
            println!("MyStruct with value {} created by pid {} tid {:?}", value, process::id(), thread::current().id());
            MyStruct { value: value }
        }
    }
    
    impl Drop for MyStruct {
        fn drop(&mut self) {
            println!("MyStruct with value {} dropped by pid {} tid {:?}", self.value, process::id(), thread::current().id());
        }
    }
    
    fn main() {
        let my_struct = MyStruct::new(42);
    }
  • trait


/*
 * 這個程式碼定義了一個名叫 Movable 的 trait,這個 trait 定義了一個 movement 方法。
 * Human 和 Rabbit 是兩個結構體,分別實現了 Movable trait。在每個實現中,
 * 都定義了一個 movement 方法,實現了結構體如何移動的行為。
 * 在 main 函數中,我們創建了一個 Human 和一個 Rabbit 的實例,存儲在 human 和 rabbit 變數中。
 * 然後,我們依次對這兩個變數分別調用了 movement 方法,分別輸出了 "Human walk" 和 "Rabbit jump"。
 * 這個程式碼展示瞭如何使用 Rust 的 trait 和結構體來實現多態行為。
 * 使用 trait,可以將類似的操作組織成一個介面,並將其實現為多個不同的類型。這使得代碼更加模組化,可重用性更高。
 * */
trait Movable {
    fn movement(&self);
}

struct Human;

impl Movable for Human {
    fn movement(&self) {
        println!("Human walk");
    }
}

struct Rabbit;

impl Movable for Rabbit {
    fn movement(&self) {
        println!("Rabbit jump");
    }
}

fn main() {
    let human = Human;
    let rabbit = Rabbit;

    human.movement();
    rabbit.movement();
}
  • Rust 中有三種方式來引用這個結構實例:self&self&mut self。下面舉例說明這三種實例引用方式的不同之處。

self&mut self` 都用於引用結構體實例,但有著不同的含義。

self 表示方法使用結構體實例的所有權;而 &mut self 則表示方法使用結構體實例的可變引用。

具體來說,當使用 self 定義方法時,這個方法會接受結構體實例的所有權,即將結構體實例移動到方法中,可以在方法內部進行修改或銷毀。當方法執行完畢後,結構體實例的控制權會返回到調用方。

而使用 &mut self 定義方法時,這個方法會接受結構體實例的可變引用。當方法被調用時,結構體實例依然保持存在,並且可以在方法內部進行修改。當方法執行完畢後,結構體實例保持存在並且可以繼續使用。

總體來說,使用 self 比使用 &mut self 更加靈活,但也更加危險,因為它轉移了結構體實例的所有權。而使用 &mut self 可以讓方法在調用時保留結構體實例,並可以在方法內部進行修改,但需要注意如果結構體同時被多個可讀寫的引用進行修改,就會產生賽博會同步錯誤。因此,方法的實現必須小心處理對結構體實例的存儲和修改依賴關係。

首先,讓我們定義一個結構 Person,其中包含了一個名稱屬性:

#![allow(unused)]
fn main() {
struct Person {
    name: String,
}
}

接下來,我們為這個結構定義三種方法,分別使用 self&self&mut self 來引用實例。

  1. 使用 self 引用實例,並修改結構屬性:
#![allow(unused)]
fn main() {
impl Person {
    fn set_name(self, new_name: String) -> Person {
        Person { name: new_name }
    }
}
}

在這個方法中,self 為結構體的值,透過使用 set_name 方法,我們可以將一個 Person 結構體的名稱屬性更改為一個新的名稱,然後返回一個新的 Person 結構體,原來的實例沒有被修改。這種方式可以原地修改實例,因為它轉移了所有權。

  1. 使用 &self 引用實例,但不修改結構屬性:
#![allow(unused)]
fn main() {
impl Person {
    fn greet(&self) {
        println!("Hi, my name is {}", self.name);
    }
}
}

在這個方法中,&self 為結構體的借用引用,它將 Person 結構體的所有權借用給了 greet 方法,但不允許 greet 方法修改該實例的任何屬性。因此,這種方式適用於只需要讀取結構屬性的方法。

  1. 使用 &mut self 引用實例,並修改結構屬性:
#![allow(unused)]
fn main() {
impl Person {
    fn rename(&mut self, new_name: String) {
        self.name = new_name;
    }
}
}

在這個方法中,&mut self 為結構體的可變引用,透過使用 rename 方法,我們可以將一個 Person 結構體的名稱屬性更改為一個新的名稱。這種方式允許修改結構屬性,因為它使用了結構體的可變引用。

總結來說,這三種方式分別提供了不同的實例引用方法。使用 self 從原始的實例移動所有權,這在歸還新創建的 Person 結構體時特別有用。使用 &self&mut self 以引用的方式讀取和修改結構屬性。使用 &self可以保證實例是不可變的,而使用 &mut self 允許修改實例的內容。

  • 在 Rust 中,selfSelf 都表示結構體或枚舉的類型,但有著不同的含義。

    self在方法定義中是用來引用實例自身,而Self則用來表示結構體或枚舉本身的類型。下面是一個示例,說明瞭Selfself` 的使用:

    在這個示例中,我們定義了一個名為 Rectangle 的結構體,並為其實現了三個方法:newareasame

    new 方法中,我們使用了 Self 來表示結構體的類型,並使用了 self 變量,它是一個引用結構體實例的不可變引用。這個方法創建了一個新的 Rectangle 結構體實例,並將其返回。

    area 方法中,我們使用了 &self 引用,這個方法只是計算結構體實例的面積,但不修改它。

    same 方法中,我們使用了 &Self 引用,這個方法不需要引用結構體實例本身,而是可以直接使用 Rectangle 類型來比較兩個實例是否具有相同的寬度和高度。

    main 函數中,我們創建了一個 Rectangle 結構體實例,調用了 area 方法來計算實例的面積,並調用了 same 方法來檢查實例是否是一個正方形。

    總結來說,self 主要用於方法中引用結構體實例本身,而 Self 則用作表示結構體或枚舉的類型。

    struct Rectangle {
        width: u32,
        height: u32,
    }
    
    impl Rectangle {
        fn new(width: u32, height: u32) -> Self {
            Self { width, height }
        }
    
        fn area(&self) -> u32 {
            self.width * self.height
        }
    
        fn same(rect: &Self) -> bool {
            rect.width == rect.height
        }
    }
    
    fn main() {
        let rectangle = Rectangle::new(10, 5);
        println!(
            "The area of the rectangle is {} square pixels.",
            rectangle.area()
        );
        println!("Is the rectangle a square? {}", Rectangle::same(&rectangle));
    }
    

    Handle trait 的方式不同

    第一個代碼示例中,我們在 Handle trait 中定義了一個實例方法 handle(),它接受一個 &self 參數,表示該方法是與 Handler 結構體實例相關聯的。在實現 Handle trait 時,我們對每個需要處理的類型都分別實現了 handle() 方法,通過 impl Handle<i32> for Handlerimpl Handle<f64> for Handler 定義了對 i32f64 類型的處理過程。在 main() 中,我們創建了 Handler 結構體對象 handler,然後調用 handler.handle(10)handler.handle(10.5) 方法來處理輸入的不同類型數據。

    struct Handler;
    
    trait Handle<T> {
        fn handle(&self, input: T);
    }
    
    impl Handle<i32> for Handler {
        fn handle(&self, input: i32) {
            println!("This is i32: {}", input);
        }
    }
    
    impl Handle<f64> for Handler {
        fn handle(&self, input: f64) {
            println!("This is f64: {}", input);
        }
    }
    
    fn main() {
        let handler = Handler;
    
        // 使用 i32 類型的 Handler
        handler.handle(10);
    
        // 使用 f64 類型的 Handler
        handler.handle(10.5);
    }

    第二個代碼示例中,我們在 Handle trait 中定義了一個關聯函數 handle(),它不需要 &self 參數,表示該函數與 Handler 結構體實例無關。在實現 Handle trait 時,我們同樣對每個需要處理的類型都分別實現了 handle() 關聯函數,通過 impl Handle<i32> for Handlerimpl Handle<f64> for Handler 定義了對 i32f64 類型的處理過程。在 main() 中,我們不創建任何 Handler 的對象,而是直接對 Handler 結構體類型調用 Handler::handle(10)Handler::handle(10.5)方法來處理輸入的不同類型數據。

    因此,這兩段代碼的區別在於實現 Handle trait 的方式不同。第一個代碼示例中實現了一個實例方法 handle(),第二個代碼示例中實現了一個關聯函數 handle()。這兩個方法/函數的調用方式也不同。

    struct Handler;
    
    trait Handle<T> {
        fn handle(&self, input: T);
    }
    
    impl Handle<i32> for Handler {
        fn handle(&self, input: i32) {
            println!("This is i32: {}", input);
        }
    }
    
    impl Handle<f64> for Handler {
        fn handle(&self, input: f64) {
            println!("This is f64: {}", input);
        }
    }
    
    fn main() {
        let handler = Handler;
    
        // 使用 i32 類型的 Handler
        handler.handle(10);
    
        // 使用 f64 類型的 Handler
        handler.handle(10.5);
    }

enum 用法

#[allow(clippy::all)]
enum WebsocketAPI {
    Default,
    MultiStream,
    Custom(String),
}

fn handle_websocket_api(api: WebsocketAPI) {
    match api {
        WebsocketAPI::Default => {
            println!("Handling default WebSocket API");
            // Your code for the default case
        }
        WebsocketAPI::MultiStream => {
            println!("Handling multi-stream WebSocket API");
            // Your code for the multi-stream case
        }
        WebsocketAPI::Custom(custom_api) => {
            println!("Handling custom WebSocket API: {}", custom_api);
            // Your code for the custom case, using the custom API string
        }
    }
}

fn main() {
    let default_api = WebsocketAPI::Default;
    let multi_stream_api = WebsocketAPI::MultiStream;
    let custom_api = WebsocketAPI::Custom(String::from("wss://custom.api"));

    handle_websocket_api(default_api);
    handle_websocket_api(multi_stream_api);
    handle_websocket_api(custom_api);
}

HashMap 用法:

HashMap是一種鍵-值對的集合,其中每個鍵必須是唯一的。它是Rust標準庫的一部分,用於實現字典或關聯數組。

這是一個使用HashMap的例子:

use std::collections::HashMap;

fn main() {
    // Creating a new HashMap
    let mut my_map = HashMap::new();

    // Inserting key-value pairs
    my_map.insert("key1", "value1");
    my_map.insert("key2", "value2");
    my_map.insert("key3", "value3");

    // Accessing values using keys
    if let Some(value) = my_map.get("key2") {
        println!("Value for key2: {}", value);
    }

    // Iterating over key-value pairs
    for (key, value) in &my_map {
        println!("Key: {}, Value: {}", key, value);
    }
}

迭代器的 map 方法:

在Rust中,迭代器具有map方法,它通過將函數應用於每個元素來轉換迭代器中的每個項目。這裡是一個簡單的例子:

在此示例中,使用map方法創建了一個新的迭代器,其中每個元素都加倍。然後使用collect方法將迭代器轉換迴向量。

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // Using map to double each number
    let doubled_numbers: Vec<_> = numbers.into_iter().map(|x| x * 2).collect();

    println!("Original numbers: {:?}", numbers);
    println!("Doubled numbers: {:?}", doubled_numbers);
}

閉包(closures)

是一種特殊的函數類型,它可以捕獲其環境中的變數。閉包具有以下幾個用法和優勢:

簡潔性和靈活性:

閉包允許你編寫更為簡潔、直觀的程式碼。相比於定義一個完整的函數,閉包可以直接在需要時聲明和使用,使程式碼更具靈活性。

#![allow(unused)]
fn main() {
// 使用閉包
let add = |x, y| x + y;
println!("Sum: {}", add(3, 4));

// 相同的功能使用函數
fn add_function(x: i32, y: i32) -> i32 {
    x + y
}
println!("Sum: {}", add_function(3, 4));
}

捕獲環境變數:

閉包可以捕獲其所在範疇中的變數,可以是引用(&)或移動(move)。這允許你在閉包內部使用外部變數,而不需要顯式傳遞參數。

#![allow(unused)]
fn main() {
let x = 10;
let closure = || println!("x: {}", x);
closure();
}

所有權轉移:

使用 move 關鍵字,閉包可以將其環境中的所有權轉移到閉包內,從而實現所有權的轉移。這對於將資料傳遞給執行緒或其他閉包非常有用。

#![allow(unused)]
fn main() {
let data = vec![1, 2, 3];
let closure = move || {
    // data 所有權已轉移到閉包
    println!("{:?}", data);
};
closure();
// 下面的行將會引發編譯錯誤,因為 data 所有權已轉移
// println!("{:?}", data);
}

函數式程式設計:

閉包使Rust更加適合函數式程式設計風格。你可以將閉包傳遞給其他函數,或者將其作為迭代器的參數。

#![allow(unused)]
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let squared: Vec<_> = numbers.into_iter().map(|x| x * x).collect();
println!("{:?}", squared);
}

泛型和trait的使用:

閉包可以與泛型和trait一起使用,使其更加通用和靈活。你可以定義一個接受閉包作為參數的泛型函數,以處理不同類型的操作。

fn perform_operation<T, U, F>(value: T, operation: F) -> U
where
    F: Fn(T) -> U,
{
    operation(value)
}

fn main() {
    // 對整數進行操作
    let result_int = perform_operation(5, |x| x * 2);
    println!("Result for integer: {}", result_int);

    // 對浮點數進行操作
    let result_float = perform_operation(3.5, |x| x * 2.0);
    println!("Result for float: {}", result_float);

    // 對字串進行操作
    let result_string = perform_operation("Hello", |x| format!("{}!", x));
    println!("Result for string: {}", result_string);
}
  1. fn perform_operation<T, U, F>(value: T, operation: F) -> U: 這是一個泛型函數的聲明。它有三個泛型參數,分別為 TUF。這表示這個函數可以接受不同類型的值(T)和返回不同類型的結果(U),同時還接受一個泛型的函數或閉包(F)。
  2. where F: Fn(T) -> U: 這是一個 trait bound(特徵約束),它規定了泛型 F 必須實現 Fn(T) -> U 這個特徵。這表示 F 必須是一個接受 T 類型參數的函數,並返回 U 類型的值。換句話說,operation 參數必須是一個可以接受 value 類型的函數或閉包。
  3. { operation(value) }: 函數體中的這一行是具體的實現。它調用了傳入的 operation 函數或閉包,並將 value 作為參數傳遞給它。整個函數最終返回 operation 的結果,這個結果的類型是 U

這段程式碼的目的是創建一個通用的函數,可以將一個值和一個函數或閉包傳遞給它,並返回該函數或閉包對該值的操作結果。通過使用泛型,這個函數可以處理不同類型的輸入和輸出。

TUF 只是慣例上常用的泛型參數名稱,實際上你可以使用任何有效的識別符號作為泛型參數名稱。這些字母通常代表不同的概念:

  • T:通常表示 "Type",表示泛型的類型參數。
  • U:通常用於表示第二個泛型類型參數。
  • F:通常表示 "Function",用於表示接受或返回函數的泛型參數。

這些僅僅是慣例,而不是強制的規則。當你閱讀其他人的代碼或寫自己的代碼時,習慣上使用這樣的字母可以讓代碼更容易閱讀和理解。

fn perform_operation<Input, Output, Func>(value: Input, operation: Func) -> Output
where
    Func: Fn(Input) -> Output,
{
    operation(value)
}

fn main() {
    // 對整數進行操作
    let result_int = perform_operation(5, |x| x * 2);
    println!("Result for integer: {}", result_int);

    // 對浮點數進行操作
    let result_float = perform_operation(3.5, |x| x * 2.0);
    println!("Result for float: {}", result_float);

    // 對字串進行操作
    let result_string = perform_operation("Hello", |x| format!("{}!", x));
    println!("Result for string: {}", result_string);
}

trait

trait是一種定義共享行為的機制,它類似於其他語言中的接口(interface)。trait可以用於定義方法簽名,然後類型實現這些trait,以提供對這些方法的具體實現。

以下是一個簡單的示例,演示瞭如何定義trait和實現它:

// 定義一個名為 Printable 的 trait
trait Printable {
    // 方法簽名,表示實現這個 trait 的類型需要實現 print 方法
    fn print(&self);
}

// 實現 Printable trait 的結構體
struct Dog {
    name: String,
}

// 實現 Printable trait 的結構體
struct Cat {
    name: String,
}

// 實現 Printable trait for Dog
impl Printable for Dog {
    // 實現 print 方法
    fn print(&self) {
        println!("Dog named {}", self.name);
    }
}

// 實現 Printable trait for Cat
impl Printable for Cat {
    // 實現 print 方法
    fn print(&self) {
        println!("Cat named {}", self.name);
    }
}

fn main() {
    // 創建一個 Dog 實例
    let dog = Dog { name: String::from("Buddy") };
    // 呼叫 Printable trait 中的 print 方法
    dog.print();

    // 創建一個 Cat 實例
    let cat = Cat { name: String::from("Whiskers") };
    // 呼叫 Printable trait 中的 print 方法
    cat.print();
}
  • 我們定義了一個名為 Printable 的 trait,它包含一個方法 print
  • 我們創建了兩個結構體 DogCat
  • 我們為每個結構體實現了 Printable trait,提供了對 print 方法的具體實現。
  • main 函數中,我們創建了一個 Dog 實例和一個 Cat 實例,然後分別呼叫了它們的 print 方法。

這就是trait的基本用法。trait還可以用於實現泛型,以及在函數中指定trait約束,這樣可以在不同類型上使用相同的trait方法。

trait 和泛型是 Rust 中的兩個不同的概念,但它們經常一起使用。

泛型(Generics)是一種通用編程概念,它允許編寫可以處理多種不同類型的代碼而不失靈活性和安全性的方式。通過使用泛型,可以在函數、結構、列舉和方法等多種場景中創建通用的代碼。

範例:

use std::fmt::Debug; // 引入 Debug trait

fn print<T: Debug>(value: T) {
    println!("Value: {:#?}", value);
}

fn main() {
    print(5);
    print("Hello");
}

這裡的 print 函數使用泛型,可以接受任何類型的參數。

Trait:

Trait 定義了一組可以由類型實現的方法的集合,這樣就可以共享某種行為。Trait 提供了一種方式來描述類型之間的共同特徵。

範例:

trait Printable {
    fn print(&self);
}

struct Dog {
    name: String,
}

impl Printable for Dog {
    fn print(&self) {
        println!("Dog named {}", self.name);
    }
}

struct Cat {
    name: String,
}

impl Printable for Cat {
    fn print(&self) {
        println!("Cat named {}", self.name);
    }
}

fn main() {
    let dog = Dog {
        name: String::from("Buddy"),
    };
    dog.print();

    let cat = Cat {
        name: String::from("Whiskers"),
    };
    cat.print();
}

在這裡,Printable 是一個 trait,DogCat 結構體實現了這個 trait,提供了對 print 方法的具體實現。

結論:

總體而言,泛型是一種更通用的編程概念,用於創建可以處理多種類型的代碼,而 trait 則用於描述類型之間的共同特徵,讓不同的類型可以共享某種行為。在實踐中,泛型和 trait 經常一起使用,使得代碼更加靈活和可擴展。

Self 與 self 差異

Self 是一個特殊的關鍵字,通常用於表示實現 trait 的類型。它表示實際類型,即實現 trait 的類型本身。使用 Self 的時機主要包括:

  1. 返回類型聲明: 當你在 trait 的方法中聲明返回類型時,可以使用 Self 來表示實現該 trait 的具體類型。這允許實現方在方法中返回其實際類型。

    #![allow(unused)]
    fn main() {
    trait ExampleTrait {
        fn example_method(&self) -> Self;
    }
    }
  2. 關聯類型: Self 也可用於關聯類型,這是一種在 trait 中聲明類型並在實現中具體化的方式。

    #![allow(unused)]
    fn main() {
    trait ExampleTrait {
        type Item;
        
        fn get_item(&self) -> Self::Item;
    }
    }

總體而言,Self 用於在 trait 中表示實現該 trait 的類型,並在需要指代實際類型的地方使用。

type 是一個關鍵字,用於聲明與trait關聯的關聯類型。關聯類型允許trait中使用的類型在實現trait時具體化。在你的例子中,type Item; 就是在trait ExampleTrait 中聲明瞭一個關聯類型 Item

trait ExampleTrait {
    type Item;  // 關聯類型聲明
    
    fn create_instance() -> Self;  // 使用Self作為返回類型
    fn get_item(&self) -> Self::Item;  // 使用Self::Item作為返回類型
}

struct ExampleType;

impl ExampleTrait for ExampleType {
    type Item = i32;  // 具體化關聯類型
    
    fn create_instance() -> Self {
        ExampleType  // 返回實現trait的具體類型
    }

    fn get_item(&self) -> Self::Item {
        42  // 在實現中返回關聯類型的實例
    }
}

fn main() {
    let instance = ExampleType::create_instance();
    let item = instance.get_item();
    
    println!("Item: {}", item);
}

python 繼承 用 Rust 實作

# 定義父類
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        pass  # 父類中的方法,子類將覆蓋它

# 定義子類,繼承自 Animal
class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

# 定義另一個子類,也繼承自 Animal
class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

# 創建實例並調用方法
dog_instance = Dog("Buddy")
cat_instance = Cat("Whiskers")

print(dog_instance.speak())  # 輸出: Buddy says Woof!
print(cat_instance.speak())  # 輸出: Whiskers says Meow!
// 定義 trait
trait Animal {
    fn new(name: &str) -> Self;
    fn speak(&self) -> String;
}

// 定義結構體實現 trait
struct Dog {
    name: String,
}

impl Animal for Dog {
    fn new(name: &str) -> Self {
        Dog {
            name: name.to_string(),
        }
    }

    fn speak(&self) -> String {
        format!("{} says Woof!", self.name)
    }
}

// 定義另一個結構體實現 trait
struct Cat {
    name: String,
}

impl Animal for Cat {
    fn new(name: &str) -> Self {
        Cat {
            name: name.to_string(),
        }
    }

    fn speak(&self) -> String {
        format!("{} says Meow!", self.name)
    }
}

fn main() {
    // 創建實例並調用方法
    let dog_instance = Dog::new("Buddy");
    let cat_instance = Cat::new("Whiskers");

    println!("{}", dog_instance.speak()); // 輸出: Buddy says Woof!
    println!("{}", cat_instance.speak()); // 輸出: Whiskers says Meow!
}

enum 跟 impl

在這個例子中,我們使用了列舉 Animal 來表示不同類型的動物(狗和貓)。每個動物類型都有一個 name 欄位。我們通過在列舉上實現方法來模擬建構函式(new_dognew_cat)和 speak 方法。在 main 函數中,我們建立了兩個不同類型的動物實例並呼叫了它們的 speak 方法。

// 定義一個枚舉,表示不同類型的動物
enum Animal {
    Dog { name: String },
    Cat { name: String },
}

// 枚舉上的方法
impl Animal {
    // 構造函數
    fn new_dog(name: &str) -> Self {
        Animal::Dog { name: name.to_string() }
    }

    fn new_cat(name: &str) -> Self {
        Animal::Cat { name: name.to_string() }
    }

    // 說話的方法
    fn speak(&self) -> String {
        match self {
            Animal::Dog { name } => format!("{} says Woof!", name),
            Animal::Cat { name } => format!("{} says Meow!", name),
        }
    }
}

fn main() {
    // 創建實例並調用方法
    let dog_instance = Animal::new_dog("Buddy");
    let cat_instance = Animal::new_cat("Whiskers");

    println!("{}", dog_instance.speak()); // 輸出: Buddy says Woof!
    println!("{}", cat_instance.speak()); // 輸出: Whiskers says Meow!
}

struct 跟 impl

在這個例子中,我們使用了 struct 定義了 Animal 結構體,其中包含了 kind 表示動物的種類("Dog" 或 "Cat"),以及 name 表示動物的名字。建構函式 new 用於建立新的 Animal 實例,而 speak 方法根據動物的種類輸出不同的聲音。在 main 函數中,我們建立了兩個不同類型的動物實例並呼叫了它們的 speak 方法。

// 定義結構體
struct Animal {
    kind: String,
    name: String,
}

// Animal 結構體的方法
impl Animal {
    // 構造函數
    fn new(kind: &str, name: &str) -> Self {
        Animal {
            kind: kind.to_string(),
            name: name.to_string(),
        }
    }

    // 說話的方法
    fn speak(&self) -> String {
        match self.kind.as_str() {
            "Dog" => format!("{} says Woof!", self.name),
            "Cat" => format!("{} says Meow!", self.name),
            _ => format!("Unknown animal"),
        }
    }
}

fn main() {
    // 創建實例並調用方法
    let dog_instance = Animal::new("Dog", "Buddy");
    let cat_instance = Animal::new("Cat", "Whiskers");

    println!("{}", dog_instance.speak()); // 輸出: Buddy says Woof!
    println!("{}", cat_instance.speak()); // 輸出: Whiskers says Meow!
}
// 定義結構體
struct Point {
    x: f64,
    y: f64,
}

// 在結構體上實現方法
impl Point {
    // 構造函數
    fn new(x: f64, y: f64) -> Point {
        Point { x, y }
    }

    // 計算兩點之間的距離
    fn distance(&self, other: &Point) -> f64 {
        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
    }

    // 移動點的位置
    fn translate(&mut self, dx: f64, dy: f64) {
        self.x += dx;
        self.y += dy;
    }
}

fn main() {
    // 創建 Point 的實例
    let point1 = Point::new(0.0, 0.0);
    let point2 = Point::new(3.0, 4.0);

    // 調用 Point 上的方法
    println!("Distance between points: {}", point1.distance(&point2));

    let mut point3 = Point::new(1.0, 1.0);
    point3.translate(2.0, 3.0);
    println!("New point location: ({}, {})", point3.x, point3.y);
}

Async Await spawn 用法

use tokio::time::{sleep, Duration};

// 異步函數
async fn async_function(id: usize) {
    println!("Start of async function {}", id);

    // 模擬異步操作,例如 I/O 操作
    sleep(Duration::from_secs(2)).await;

    println!("End of async function {}", id);
}

// 您可以一次連續呼叫 async_function 多次。在非同步程式設計中,您可以使用 tokio::spawn 或其他類似的功能來並行執行多個非同步任務。下面是一個例子,演示如何連續呼叫 async_function 5 次
#[tokio::main]
async fn main() {
    println!("Start of main function");

    // 創建一個 Vec 來存儲任務句柄
    let mut handles = Vec::new();

    // 調用 async_function 5 次
    for i in 0..5 {
        // 使用 tokio::spawn 啟動異步任務,並將任務句柄存儲在 Vec 中
        let handle = tokio::spawn(async_function(i));
        handles.push(handle);
    }

    // 等待所有任務完成
    for handle in handles {
        handle.await.expect("Failed to await task");
    }

    println!("End of main function");
}
[package]
name = "rust_async_test"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tokio = { version = "1", features = ["full"] }

into

Into trait 是一種常見的用於類型轉換的 trait,但並不是唯一的方式。Rust 還提供了一種簡化類型轉換的手段,即使用 Into trait 的 into 方法。

在 Rust 中,對於任何實現了 From trait 的類型,都可以使用 into 方法進行類型轉換。這是因為 Into trait 是 From trait 的逆。具體來說,Into<T> trait 的實現是由 T 實現的 From<U> trait 決定的。

// 定義一個結構 Point
struct Point {
    x: i32,
    y: i32,
}

// 實現 Into<T> trait for Point
impl Into<(i32, i32)> for Point {
    fn into(self) -> (i32, i32) {
        (self.x, self.y)
    }
}

fn main() {
    // 創建一個 Point 實例
    let point = Point { x: 10, y: 20 };

    // 使用 .into() 將 Point 轉換成 (i32, i32)
    let tuple: (i32, i32) = point.into();

    // 打印轉換後的結果
    println!("Tuple: {:?}", tuple);
}
// 定義一個結構 Point
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    // 創建一個 Point 實例
    let point = Point { x: 10, y: 20 };

    // 使用 .into() 將 Point 轉換成 (i32, i32)
    let tuple: (i32, i32) = point.into();

    // 打印轉換後的結果
    println!("Tuple: {:?}", tuple);
}

struct 用於定義結構體(structure),即一種用來組織和存儲數據的自定義類型。而 trait 則用於定義接口,即一組方法的集合,這些方法可以被實現在各種不同的類型上。

// 定義特徵 Displayable
trait Displayable {
    fn display(&self);
}

// 實現特徵 Displayable for Point
struct Point {
    x: f64,
    y: f64,
}

impl Displayable for Point {
    fn display(&self) {
        println!("Point: ({}, {})", self.x, self.y);
    }
}

// 實現特徵 Displayable for Circle
struct Circle {
    radius: f64,
}

impl Displayable for Circle {
    fn display(&self) {
        println!("Circle with radius: {}", self.radius);
    }
}

// 定義特徵 Add
trait Add {
    fn add(&self, other: &Self) -> Self;
}

// 實現特徵 Add for i32
impl Add for i32 {
    fn add(&self, other: &Self) -> Self {
        *self + *other
    }
}

// 實現特徵 Add for f64
impl Add for f64 {
    fn add(&self, other: &Self) -> Self {
        *self + *other
    }
}

// 定義特徵 Double
trait Double {
    fn double(&self) -> Self;
}

// 實現特徵 Double for i32
impl Double for i32 {
    fn double(&self) -> Self {
        *self * 2
    }
}

// 實現特徵 Double for f64
impl Double for f64 {
    fn double(&self) -> Self {
        *self * 2.0
    }
}

// 主函數
fn main() {
    // 使用 Displayable 特徵的方法
    let point = Point { x: 1.0, y: 2.0 };
    point.display();

    let circle = Circle { radius: 3.0 };
    circle.display();

    // 使用 Add 特徵的方法
    let sum_i32 = 10i32.add(&5);
    let sum_f64 = 3.5f64.add(&2.5);

    println!("Sum of i32: {}", sum_i32);
    println!("Sum of f64: {}", sum_f64);

    // 使用 Double 特徵的方法
    let doubled_i32 = 7i32.double();
    let doubled_f64 = 4.2f64.double();

    println!("Doubled i32: {}", doubled_i32);
    println!("Doubled f64: {}", doubled_f64);
}
// 定義特徵 Add
trait Add {
    fn add(&self, other: &Self) -> Self;
}

// 實現特徵 Add for i32
impl Add for i32 {
    fn add(&self, other: &Self) -> Self {
        *self + *other
    }
}

// 實現特徵 Add for f64
impl Add for f64 {
    fn add(&self, other: &Self) -> Self {
        *self + *other
    }
}

// 主函數
fn main() {
    // 使用 Add 特徵的方法
    let sum_i32 = 10i32.add(&5);
    let sum_f64 = 3.5f64.add(&2.5);

    println!("Sum of i32: {}", sum_i32);
    println!("Sum of f64: {}", sum_f64);
}

多型(polymorphism)

在 Rust 中通常是通過 trait 和泛型實現的。下面是一個簡單的多型範例,其中使用了 trait 和泛型,允許一個函數接受不同類型的參數:

// 定義一個特徵 Display
trait Display {
    fn display(&self);
}

// 實現 Display 特徵的結構體 Point
struct Point {
    x: f64,
    y: f64,
}

impl Display for Point {
    fn display(&self) {
        println!("Point: ({}, {})", self.x, self.y);
    }
}

// 實現 Display 特徵的結構體 Circle
struct Circle {
    radius: f64,
}

impl Display for Circle {
    fn display(&self) {
        println!("Circle with radius: {}", self.radius);
    }
}

// 多型函數,接受實現 Display 特徵的任意類型
fn show_displayable<T: Display>(item: T) {
    item.display();
}

fn main() {
    let point = Point { x: 1.0, y: 2.0 };
    let circle = Circle { radius: 3.0 };

    // 調用多型函數,可以接受不同類型的參數
    show_displayable(point);
    show_displayable(circle);
}

泛型

// 定義一個泛型函數,接受兩個參數並返回它們的和
fn add<T>(a: T, b: T) -> T
where
    T: std::ops::Add<Output = T>,
{
    a + b
}

fn main() {
    // 使用泛型函數,可以處理不同類型的數據
    let sum_i32 = add(5, 3);
    let sum_f64 = add(3.5, 2.5);

    println!("Sum of i32: {}", sum_i32);
    println!("Sum of f64: {}", sum_f64);
}

在這個範例中,add 函數是一個泛型函數,它接受兩個相同類型的參數 ab,並返回它們的和。泛型參數 T 表示可以是任何類型。where T: std::ops::Add<Output = T> 確保 T 實現了 Add trait,並指定了 Output 類型為 T

main 函數中,我們分別使用整數和浮點數調用了 add 函數,顯示了泛型函數可以處理不同類型的數據並返回正確的結果。

在上述的泛型範例中,where T: std::ops::Add<Output = T> 是一個泛型約束(generic constraint)子句,用於指定泛型參數 T 必須滿足的條件。

這個約束的意義是,泛型參數 T 必須實現 std::ops::Add trait,並且其 Add 實現的輸出類型(Output)必須是 T。換句話說,T 只能與自己相加,而不是與其他類型相加。

這樣的約束確保了 add 函數在編譯時期只能被用於那些支持 + 運算的類型,並保證了在編譯時期就能夠確定 add 函數的行為。

不使用 where 子句的版本可能看起來像這樣:

#![allow(unused)]
fn main() {
fn add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T {
    a + b
}
}
fn multiply_value<T>(value: T, factor: T) -> T
where
    T: std::ops::Mul<Output = T>,
{
    value * factor
}

fn main() {
    let integer_result = multiply_value(5, 3);
    let float_result = multiply_value(3.5, 2.0);

    println!("Result of multiplying integers: {}", integer_result);
    println!("Result of multiplying floats: {}", float_result);
}

where 子句的存在讓約束條件更為清晰,有時可以提高代碼的可讀性,特別是當約束條件較長或較複雜時。這種寫法的主要優勢是可以將約束從函數的簽名中分離出來,讓簽名更加簡潔。

總體而言,where 子句的使用是為了確定泛型參數滿足特定的條件,提高代碼的可讀性和可維護性。

#[derive(Debug)]

使用 #[derive(Debug)] 時,Rust 編譯器會自動生成一個 Debug trait 的實現。這個生成的實現通常包含一個 fmt::Debug trait 的 fmt 方法,該方法負責將類型的偵錯表示格式化為字串。

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Point {
    x: f64,
    y: f64,
}
}

當你使用 #[derive(Debug)] 註解時,Rust 編譯器會自動生成類似以下的程式碼:

#![allow(unused)]
fn main() {
impl std::fmt::Debug for Point {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // 使用 Formatter 將調試信息格式化為字符串
        f.debug_struct("Point")
            .field("x", &self.x)
            .field("y", &self.y)
            .finish()
    }
}
}

這個生成的實現為 Point 類型實現了 Debug trait 中的 fmt 方法。在這個方法中,使用了 std::fmt::Debug 中提供的 debug_structfieldfinish 方法來建構偵錯表示。具體來說:

  • debug_struct("Point") 建立一個名為 "Point" 的偵錯結構體。
  • field("x", &self.x) 新增一個名為 "x" 的欄位,並將 self.x 的偵錯表示新增到結構體中。
  • field("y", &self.y) 同樣新增一個名為 "y" 的欄位,並將 self.y 的偵錯表示新增到結構體中。
  • finish() 完成結構體的建構,生成最終的偵錯表示。

這樣,當你使用 println! 宏並使用 {:?} 預留位置列印 Point 類型的實例時,編譯器自動生成的 Debug trait 實現將被呼叫,輸出類似於 Point { x: 3.0, y: 4.0 } 的偵錯資訊。這種自動生成的實現簡化了偵錯過程,使得偵錯資訊更加易讀和友好。

Result

Result 是一個列舉類型,用於表示函數執行的結果,特別是可能發生錯誤的情況。Result 的定義如下:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

這裡有兩個變數,TET 代表成功時返回的值的類型,而 E 代表錯誤時返回的值的類型。Result 列舉有兩個變體:

  1. Ok(T): 表示操作成功,包含一個成功時返回的值 T
  2. Err(E): 表示操作發生錯誤,包含一個錯誤時返回的值 E

例如,一個函數可能返回 Result 類型來表示執行結果:

#![allow(unused)]
fn main() {
fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
    if b == 0 {
        // 如果嘗試除以零,則返回一個 Err 變體,包含錯誤信息
        Err("Cannot divide by zero!")
    } else {
        // 如果成功,返回 Ok 變體,包含結果值
        Ok(a / b)
    }
}
}

Result<i32, &'static str> 是一個 Result 類型的實例,表示可能返回一個整數(i32類型)的成功結果,或者返回一個靜態字串切片(&'static str類型)的錯誤資訊。

讓我們詳細解釋這個類型:

  • Result: 這是Rust標準庫中的列舉類型,用於表示操作的結果,可以是成功的值(Ok變體)或錯誤的值(Err變體)。
  • <i32, &'static str>: 這是 Result 的兩個類型參數。第一個參數 i32 表示成功時返回的值的類型,第二個參數 &'static str 表示錯誤時返回的值的類型。&'static str 是一個指向靜態字串切片的引用,通常用於表示在整個程序生命週期中都有效的字串。

在使用 Result 類型時,通常會使用模式匹配(pattern matching)或 Result 的方法來處理操作的成功和失敗情況。例如:

#![allow(unused)]
fn main() {
match divide(10, 2) {
    Ok(result) => println!("Result: {}", result),
    Err(err) => println!("Error: {}", err),
}
}

或者使用 unwrapexpectmapand_then 等方法進行更複雜的處理。這樣的設計使得在Rust中明確處理可能發生的錯誤,避免了在執行階段出現未處理的異常。這也是Rust中的一種推崇錯誤處理的哲學,通過強制使用 Result 類型鼓勵開發者更加關注和處理潛在的錯誤情況。

模擬 Result 列舉

enum _Result<T, E> {
    Ok(T),
    Err(E),
}

fn divide(a: i32, b: i32) -> _Result<i32, &'static str> {
    if b == 0 {
        // 如果嘗試除以零,則返回一個 Err 變體,包含錯誤信息
        _Result::Err("Cannot divide by zero!")
    } else {
        // 如果成功,返回 Ok 變體,包含結果值
        _Result::Ok(a / b)
    }
}

fn main() {
    match divide(10, 2) {
        _Result::Ok(result) => println!("Result: {}", result),
        _Result::Err(err) => println!("Error: {}", err),
    }
}

Rust中字串操作的擁有權和可變引用

這兩個程式碼的主要區別在於函數 greet 如何處理字串的擁有權(ownership):

第一個範例:

rustCopy codefn greet(mut message: String) -> String {
    message.push_str(", World!"); // 修改字串的內容
    println!("{}", message);
    message
}

fn main() {
    let message = String::from("Hello");
    println!("{}", message);
    let msg = greet(message);
    println!("{}", msg);
}
  1. greet 函數接受一個 String 的擁有權,對它進行修改(push_str),然後返回修改後的字串,同時也轉移了擁有權。
  2. main 函數中,message 的擁有權在調用 greet 函數時轉移到了 greet 內,因此 main 無法再使用原始的 message

第二個範例:

rustCopy codefn greet(message: &mut String) {
    message.push_str(", World!"); // 修改字串的內容
    println!("{}", message);
}

fn main() {
    let mut message = String::from("Hello");
    println!("{}", message);

    greet(&mut message); // 傳遞字串的可變引用
    println!("{}", message);
}
  1. greet 函數接受一個 &mut String,這是字串的可變引用,它允許 greet 修改字串的內容,但不轉移擁有權。
  2. main 函數中,message 保留了擁有權,你可以通過傳遞 &mut message 來傳遞對字串的可變引用,使 greet 能夠修改字串的內容。
  3. main 仍然擁有 message 並且可以在 greet 被呼叫後繼續使用修改後的 message

總的來說,第二個範例使用了引用和可變引用,保留了 message 的擁有權,允許在函數間進行資料的共享,而不是轉移擁有權。

Borrow Checker 的限制

Rust 的 Borrow Checker 的限制,這是為了確保在編譯時能夠避免數據競爭和安全性問題。具體來說,在同一個作用域中,你不能同時擁有兩個可變引用指向同一個值。這就是為什麼你無法同時擁有 a 和 b 兩個可變引用指向 array 的兩個元素的原因。

Rust 提供了一些方法來處理這種情況,其中一個方法是使用 .split_at_mut() 方法來將陣列分成兩個不重疊的可變引用。這裡是如何修改你的程式碼以解決這個問題:

fn main() {
    let mut array = [123, 456];
    
    // 將陣列拆分成兩個可變引用,分別指向不同的元素
    let (a, b) = array.split_at_mut(1);
    let a = &mut a[0];
    let b = &mut b[0];
    
    *a = 789;
    *b = 101112;
    println!("{:?}", array);
}

如何使用 ArcMutex 在多個線程之間安全地共享和修改一個可變的向量數據結構。

use std::sync::{Arc, Mutex};
use std::thread;
/*
 * // 克隆 Arc,以便在兩個線程間共享
 * let shared_data1 = Arc::clone(&shared_data);
 * let shared_data2 = Arc::clone(&shared_data);
 * Arc:clone 作用
 * Arc::clone(&shared_data) 是用於在多個線程之間安全地共享資料的方法。
 * Arc 是 Rust 標準庫提供的一種參考計數智慧型指標類型,全稱是 Atomic Reference Counted。它的作用是在多個線程間共享所有權,並在沒有線程再使用資料時自動釋放其記憶體。
 * 當你調用 Arc::clone(&shared_data) 時,它會建立一個新的 Arc 指標,指向與原始 shared_data 相同的堆上資料。這個新指標的參考計數會增加,表示有一個新的所有者。
 * 這種參考計數機制確保了只要有一個 Arc 指標存在,就不會釋放底層資料。當最後一個 Arc 指標被丟棄時,參考計數會歸零,底層資料的記憶體才會被自動釋放。
 * 通過在多個線程中克隆 Arc 指標,你可以安全地在它們之間共享資料,而無需深度複製資料或手動管理其生命週期。這在需要在線程間傳遞所有權的情況下非常有用。
 * 總的來說,Arc::clone 讓你可以建立指向相同資料的多個 Arc 指標,從而在線程間共享資料,而不會產生資料競爭或違反所有權規則。
 *
 */

// let mut data = shared_data1.lock().unwrap();` 這行代碼的作用是從 `Arc` 智慧指標中取得可變引用資料的存取權。讓我們仔細解釋一下:
// 1. `shared_data1` 是一個 `Arc<T>` 類型的智慧指標,它封裝了資料 `T` 並實現了資料的引用計數和線程間安全存取。
// 2. `.lock()` 方法是由 `Arc<T>` 實現的,它返回一個 `LockResult` 類型,代表對內部資料 `T` 的存取權。
// 3. `.unwrap()` 是解開 `Result` 類型。如果 `lock()` 成功獲取存取權,它會返回 `Ok(MutexGuard)` 類型,表示獲得了可變引用 `&mut T`。如果失敗返回 `Err(PoisonError)`。`.unwrap()` 會直接解開這個 `Ok` 並取得 `MutexGuard`。
// 4. `let mut data = ...` 這一行賦值,將從 `lock()` 中獲取的可變引用 `&mut T` 賦值給 `data`。
//
// 所以這行代碼的作用是:
// 1) 通過 `Arc` 智慧指標存取內部資料
// 2) 獲取該資料的互斥可變引用 (避免資料競爭)
// 3) 將可變引用賦給新變數 `data`
// 這樣就可以安全地在單個線程中修改資料,同時其他線程無法同時修改,從而避免資料競爭問題。當 `data` 離開作用域時,可變引用會自動被釋放。

fn main() {
    // 創建一個共享的可變數據結構
    let shared_data = Arc::new(Mutex::new(vec![1, 2, 3]));

    // 克隆 Arc,以便在兩個線程間共享
    let shared_data1 = Arc::clone(&shared_data);
    let shared_data2 = Arc::clone(&shared_data);

    // 在兩個線程中分別修改數據
    let thread1 = thread::spawn(move || {
        let mut data = shared_data1.lock().unwrap();
        data.push(4);
    });

    let thread2 = thread::spawn(move || {
        let mut data = shared_data2.lock().unwrap();
        data.push(5);
    });

    // 等待兩個線程完成
    thread1.join().unwrap();
    thread2.join().unwrap();

    // 打印最終結果
    println!("{:?}", shared_data.lock().unwrap());
}

使用隊列(queue)在兩個線程之間傳遞資料。Rust 標準庫提供了 std::sync::mpsc 模組來實現多生產者單消費者(Multiple Producer Single Consumer, MPSC)通道。

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    // 創建一個通道,這會產生一個發送端和一個接收端
    let (tx, rx) = mpsc::channel();

    // 啟動一個新的線程作為生產者
    let sender = thread::spawn(move || {
        // 向通道中發送一些資料
        tx.send(1).unwrap();
        // 等待一段時間
        thread::sleep(Duration::from_secs(3));
        tx.send(2).unwrap();
        tx.send(3).unwrap();
    });

    // 在主線程中接收資料
    for received in rx {
        println!("Received: {}", received);
    }

    // 等待生產者線程結束
    sender.join().unwrap();
}

示例 1:使用可變引用

fn modify_buffer(buffer: &mut [u8]) {
    buffer[0] = 1;
}

fn main() {
    let buf = &mut [0u8; 1024];
    modify_buffer(buf);
}

說明

  1. 函數聲明

    #![allow(unused)]
    fn main() {
    fn modify_buffer(buffer: &mut [u8]) {
        buffer[0] = 1;
    }
    }
    • 這個函數接受一個可變引用 &mut [u8],指向一個 u8 陣列。
    • 函數將陣列的第一個元素設定為 1。
  2. main 函數

    fn main() {
        let buf = &mut [0u8; 1024];
        modify_buffer(buf);
    }
    • let buf = &mut [0u8; 1024]; 建立一個長度為 1024 的 u8 陣列,並建立一個指向這個陣列的可變引用 buf
    • modify_buffer(buf); 呼叫 modify_buffer 函數,將 buf 傳遞給它。這傳遞的是陣列的引用,而不是陣列本身。

示例 2:直接使用可變陣列

fn modify_buffer(buffer: &mut [u8; 1024]) {
    buffer[0] = 1;
}

fn main() {
    let mut buf = [0u8; 1024];
    modify_buffer(&mut buf);
}

說明

  1. 函數聲明

    #![allow(unused)]
    fn main() {
    fn modify_buffer(buffer: &mut [u8; 1024]) {
        buffer[0] = 1;
    }
    }
    • 這個函數接受一個可變引用 &mut [u8; 1024],指向一個長度為 1024 的 u8 陣列。
    • 函數將陣列的第一個元素設定為 1。
  2. main 函數

    fn main() {
        let mut buf = [0u8; 1024];
        modify_buffer(&mut buf);
    }
    • let mut buf = [0u8; 1024]; 建立一個長度為 1024 的可變 u8 陣列 buf
    • modify_buffer(&mut buf); 呼叫 modify_buffer 函數,將 buf 的可變引用傳遞給它。

主要差異

  1. 參數類型
    • 示例 1 的 modify_buffer 函數接受一個切片 &mut [u8],這意味著它可以處理任何長度的可變 u8 陣列。
    • 示例 2 的 modify_buffer 函數接受一個固定長度的陣列 &mut [u8; 1024],只能處理長度為 1024 的陣列。
  2. 靈活性
    • 示例 1 更靈活,因為它可以處理不同長度的陣列,只要它們是 u8 類型的切片。
    • 示例 2 更固定,只能處理長度為 1024 的陣列。
  3. 記憶體管理
    • 兩個示例中的陣列都是在棧上分配的。沒有使用 BoxVec 進行堆分配。

何時使用哪種方式

  • 如果你需要處理不同長度的陣列或切片,示例 1 的方式更合適。
  • 如果你只需要處理固定長度的陣列,示例 2 的方式更簡單和直接。

選擇哪種方式取決於你的具體需求。如果需要更多靈活性和通用性,使用切片。如果只需要處理固定大小的陣列,直接使用陣列更好。


雖然 Rust 的所有權規則和內存管理與棧(stack)和堆(heap)的概念有一定關係,但不能簡單地用“棧上的變量就是 Copy,堆上的變量就是 Move”來判斷。關鍵在於類型是否實現了 Copy trait,而不是變量存儲在棧還是堆上。

棧和堆的區別

  • 棧(Stack):用於存儲函數調用和局部變量,具有後進先出(LIFO)原則。棧上分配的內存是自動管理的,且開銷小、速度快。
  • 堆(Heap):用於動態分配內存,適用於大小在編譯時不確定的對象。堆上的內存分配和釋放需要程序員手動管理或通過垃圾回收機制進行管理。

CopyMove 的區別

  • Copy trait:實現了 Copy trait 的類型在賦值或傳遞時會執行按位複製。簡單的標量類型(如整數、浮點數、布爾值、字符)和複合類型(如元組,只要其所有元素都實現了 Copy)都實現了 Copy trait。
  • 所有權轉移(Move):對於沒有實現 Copy trait 的類型(如 StringVec<T> 等),賦值或傳遞時會轉移所有權,而不是按位複製。

示例

棧上的 Copy 類型

fn main() {
    let x = 5;
    let y = x; // x 是一個實現了 Copy trait 的整數類型
    println!("x: {}, y: {}", x, y); // x 和 y 都可以正常使用,因為 x 是被複制的
}

堆上的 Move 類型

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 是 String 類型,沒有實現 Copy trait
    // println!("{}", s1); // 這行會導致編譯錯誤,因為 s1 的所有權已被轉移到 s2
    println!("{}", s2); // s2 可以正常使用
}

綜合考慮棧和堆

即使某些類型的數據存儲在堆上,這些類型的變量仍然在棧上。重要的是,變量本身(指向堆數據的指針)是否實現了 Copy trait。

結構體示例

#[derive(Debug, Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 10, y: 20 };
    let p2 = p1; // Point 實現了 Copy trait,因此是按位複製
    println!("{:?}", p1); // p1 可以正常使用
    println!("{:?}", p2); // p2 也可以正常使用
}
struct Custom {
    data: String,
}

fn main() {
    let c1 = Custom { data: String::from("hello") };
    let c2 = c1; // Custom 沒有實現 Copy trait,因此是所有權轉移
    // println!("{:?}", c1); // 這行會導致編譯錯誤,因為 c1 的所有權已被轉移到 c2
    println!("{:?}", c2); // c2 可以正常使用
}

總結來說,判斷變量是 Copy 還是 Move 不能僅通過其存儲在棧上還是堆上來決定,而是要看該類型是否實現了 Copy trait。對於實現了 Copy trait 的類型,賦值和傳遞時會進行按位複製;對於未實現 Copy trait 的類型,賦值和傳遞時會進行所有權轉移。


使用 Rust 特徵(Traits)和組合(Composition)來模擬繼承的一個完整範例

// 定義一個特徵 Animal,表示動物的行為
trait Animal {
    fn speak(&self); // 定義一個方法 speak,沒有默認實現
}

// 定義一個結構體 Dog,表示狗
struct Dog;

// 為結構體 Dog 實現特徵 Animal
impl Animal for Dog {
    fn speak(&self) {
        println!("汪汪!"); // 狗的具體實現,打印 "汪汪!"
    }
}

// 定義另一個結構體 Cat,表示貓
struct Cat;

// 為結構體 Cat 實現特徵 Animal
impl Animal for Cat {
    fn speak(&self) {
        println!("喵喵!"); // 貓的具體實現,打印 "喵喵!"
    }
}

// 定義一個通用函數,接受一個實現了 Animal 特徵的引用
fn make_animal_speak(animal: &dyn Animal) {
    animal.speak(); // 調用特徵的方法
}

// 定義一個結構體 Engine,表示引擎
struct Engine {
    horsepower: u32, // 引擎的馬力
}

// 定義一個結構體 Car,表示車輛
struct Car {
    engine: Engine, // 車輛包含一個引擎
    model: String, // 車輛的型號
}

// 為結構體 Car 定義方法
impl Car {
    fn start(&self) {
        println!(
            "{} 的引擎擁有 {} 馬力正在啟動!",
            self.model, self.engine.horsepower
        );
    }
}

// 主函數
fn main() {
    // 創建一個 Dog 實例
    let dog = Dog;
    // 創建一個 Cat 實例
    let cat = Cat;

    // 調用通用函數,使動物發聲
    make_animal_speak(&dog); // 輸出: 汪汪!
    make_animal_speak(&cat); // 輸出: 喵喵!

    // 創建一個 Engine 實例
    let engine = Engine { horsepower: 150 };
    // 創建一個 Car 實例
    let car = Car {
        engine,
        model: String::from("Toyota"),
    };

    // 啟動車輛
    car.start(); // 輸出: Toyota 的引擎擁有 150 馬力正在啟動!
}

以下是幾種解決方案:

  1. 返回所有權 讓函數返回傳入的 String,這樣可以將所有權返還給呼叫者。
fn say_hello(name: String) -> String {
    println!("Hello {name}");
    name
}

fn main() {
    let name = String::from("Alice");
    let name = say_hello(name);
    let name = say_hello(name);
}
  1. 傳遞引用 如果你不需要在函數內部修改 String,可以傳遞一個引用,這樣所有權不會轉移。
fn say_hello(name: &String) {
    println!("Hello {name}");
}

fn main() {
    let name = String::from("Alice");
    say_hello(&name);
    say_hello(&name);
}
  1. 使用 clone 你可以使用 clone 方法來建立一個 String 的深複製,並傳遞它給函數。這樣你可以保留原來的變數,但會有額外的記憶體開銷。
fn say_hello(name: String) {
    println!("Hello {name}");
}

fn main() {
    let name = String::from("Alice");
    say_hello(name.clone());
    say_hello(name.clone());
}

了解這些概念後,我可以為你提供一些簡單的 C++ 範例來說明資料複製和轉移的行為。以下是相關範例及其解釋:

1. C++ 中的資料複製

在 C++ 中,當使用 = 進行賦值時,通常會執行資料複製。如果涉及到堆積記憶體分配(例如字串、動態陣列等),這樣的複製行為會影響資源管理。

#include <iostream>
#include <string>

int main() {
    std::string s1 = "Hello, World!";
    std::string s2 = s1;  // 複製 s1 到 s2

    std::cout << "s1: " << s1 << std::endl;
    std::cout << "s2: " << s2 << std::endl;

    return 0;
}

解釋

  • s2 = s1 時,s1 的資料會被複製到 s2。此時,s1 和 s2 各自擁有一份獨立的字串資料。如果 s1 被釋放,s2 仍然有效且可用。

2. C++ 中的資料轉移

C++ 中使用 std::move 來明確表示資料應該被轉移而非複製。

#include <iostream>
#include <string>

int main() {
    std::string s1 = "Hello, World!";
    std::string s2 = std::move(s1);  // 轉移 s1 到 s2

    std::cout << "s1: " << s1 << std::endl;  // s1 處於有效但未指定的狀態
    std::cout << "s2: " << s2 << std::endl;  // s2 擁有原來 s1 的資料

    return 0;
}

解釋

  • s2 = std::move(s1) 時,s1 的資料會被轉移到 s2。這意味著 s2 現在擁有 s1 的資源,而 s1 處於有效但未指定的狀態(內容可能是空的,但未定義)。
  • 與 Rust 不同的是,C++ 中的 s1 雖然已經被轉移,但仍然可以使用,不會導致程序崩潰。

3. 複製和轉移的行為差異

C++ 的 = 運算符可以依據型別來決定是複製還是轉移。例如,如果使用的是基本型別,則會進行複製,而對於支持轉移語義的型別(如 std::string),則會根據上下文決定是否進行轉移。

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v1 = {1, 2, 3, 4, 5};
    std::vector<int> v2 = std::move(v1);  // 轉移 v1 到 v2

    std::cout << "v1 size: " << v1.size() << std::endl;  // v1 處於有效但未指定的狀態
    std::cout << "v2 size: " << v2.size() << std::endl;  // v2 擁有原來 v1 的資料

    return 0;
}

解釋

  • 在這個例子中,v1 的資料會被轉移到 v2,使得 v1 成為一個空的容器,而 v2 則擁有原來 v1 的資料。
  • std::move 允許程式設計師明確表示希望轉移所有權,而非進行昂貴的資料複製操作。

這些範例展示了 C++ 中資料複製和轉移的基本概念,並且突顯了 C++ 與 Rust 在處理所有權和資源管理方面的一些差異。

在 Rust 中,所有權、借用和生命週期是核心概念,這些機制有助於避免記憶體洩漏和資料競爭。Rust 的所有權系統確保在編譯時檢查並強制執行資源管理的規則。

1. Rust 中的所有權轉移

在 Rust 中,當將一個變數賦值給另一個變數時,所有權會被轉移(移動),而不是複製。

fn main() {
    let s1 = String::from("Hello, World!");
    let s2 = s1;  // s1 的所有權被轉移到 s2

    // println!("{}", s1);  // 這一行會導致編譯錯誤,因為 s1 的所有權已經被轉移
    println!("{}", s2);  // s2 現在擁有原來 s1 的所有權
}

解釋

  • 在這個例子中,s1 的所有權被轉移給 s2,因此 s1 在此之後不再有效。如果你試圖使用 s1,編譯器會報錯,提示 s1 已經被轉移。
  • Rust 的所有權轉移避免了重複釋放記憶體的問題,確保資源在一個特定時間點只能有一個所有者。

2. Rust 中的資料複製

在 Rust 中,只有實現了 Copy trait 的類型(如整數、浮點數、布爾值等)可以進行淺複製(按位複製)。這些類型不會發生所有權轉移。

fn main() {
    let x = 5;
    let y = x;  // x 被複製到 y

    println!("x = {}, y = {}", x, y);  // x 和 y 都有效,因為它們是 Copy 類型
}

解釋

  • 由於整數是 Copy 類型,當 x 賦值給 y 時,x 的值會被複製,而不是轉移。因此,xy 都可以繼續使用。

3. Rust 中的借用與引用

Rust 提供了引用來借用資料而不轉移所有權。這使得可以安全地共享資料。

fn main() {
    let s1 = String::from("Hello, World!");
    let s2 = &s1;  // s1 的不可變引用被借用給 s2

    println!("s1 = {}, s2 = {}", s1, s2);  // s1 和 s2 都可以被使用
}

解釋

  • 在這個例子中,s2s1 的引用(借用),這意味著 s2 可以訪問 s1 的資料,而不會轉移所有權。Rust 確保在引用的生命週期內,原變數(s1)不會被修改或轉移。

4. Rust 中的可變借用

Rust 允許可變借用,但同一時間只能有一個可變借用,這防止了資料競爭。

fn main() {
    let mut s1 = String::from("Hello, World!");
    let s2 = &mut s1;  // s1 的可變引用被借用給 s2

    s2.push_str("!!!");  // 可以通過 s2 修改 s1 的值
    println!("s2 = {}", s2);  // s2 擁有對 s1 的可變借用
}

解釋

  • 在這裡,s2s1 的可變引用,允許通過 s2 修改 s1 的值。在可變借用期間,Rust 確保不會有其他引用可以訪問 s1,這樣避免了資料競爭。

5. Rust 中的克隆

如果需要在 Rust 中顯式複製非 Copy 類型的資料,可以使用 .clone() 方法。

fn main() {
    let s1 = String::from("Hello, World!");
    let s2 = s1.clone();  // 顯式複製 s1 到 s2

    println!("s1 = {}, s2 = {}", s1, s2);  // s1 和 s2 都可以被使用
}

解釋

  • clone 方法會進行深複製,複製堆上的資料,讓 s1s2 各自擁有一份獨立的資料。這避免了所有權轉移的問題,使得兩者都可以在之後使用。

這些範例展示了 Rust 如何通過所有權和借用機制來管理資源,防止記憶體洩漏和資料競爭,同時與 C++ 的做法形成了鮮明對比。

Rust 異步編程示例

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Wake, Waker};
use std::sync::{Arc, Mutex};
use std::thread;
use std::sync::mpsc::{channel, Sender, Receiver};

// 定義一個簡單的 Future
struct CountDown {
    count: u32,
}

impl Future for CountDown {
    type Output = u32;
    
    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        println!("正在執行 poll,當前計數: {}", self.count);
        
        if self.count == 0 {
            Poll::Ready(0)
        } else {
            self.count -= 1;
            if self.count == 0 {
                Poll::Ready(0)
            } else {
                // 在真實情況下,我們應該在這裡安排一個喚醒(wake)
                let waker = cx.waker().clone();
                thread::spawn(move || {
                    thread::sleep(std::time::Duration::from_millis(500));
                    println!("喚醒 Future");
                    waker.wake();
                });
                
                Poll::Pending
            }
        }
    }
}

// 定義一個異步函數
async fn simple_async_function() -> u32 {
    println!("進入異步函數");
    let countdown = CountDown { count: 3 };
    let result = countdown.await;
    println!("異步函數完成,結果: {}", result);
    result
}

// 一個簡單的執行器實現
struct SimpleExecutor {
    task_sender: Sender<Arc<SimpleFutureTask>>,
    task_receiver: Receiver<Arc<SimpleFutureTask>>,
}

struct SimpleFutureTask {
    future: Mutex<Option<Pin<Box<dyn Future<Output = ()> + Send>>>>,
    task_sender: Sender<Arc<SimpleFutureTask>>,
}

impl Wake for SimpleFutureTask {
    fn wake(self: Arc<Self>) {
        let _ = self.task_sender.send(self.clone());
    }
}

impl SimpleExecutor {
    fn new() -> Self {
        let (task_sender, task_receiver) = channel();
        SimpleExecutor {
            task_sender,
            task_receiver,
        }
    }

    fn spawn<F>(&self, future: F)
    where
        F: Future<Output = ()> + Send + 'static,
    {
        let task = Arc::new(SimpleFutureTask {
            future: Mutex::new(Some(Box::pin(future))),
            task_sender: self.task_sender.clone(),
        });
        let _ = self.task_sender.send(task);
    }

    fn run(&self) {
        while let Ok(task) = self.task_receiver.recv() {
            let mut future_slot = task.future.lock().unwrap();
            if let Some(mut future) = future_slot.take() {
                let waker = Waker::from(task.clone());
                let mut cx = Context::from_waker(&waker);
                match Future::poll(Pin::new(&mut future), &mut cx) {
                    Poll::Pending => {
                        *future_slot = Some(future);
                    }
                    Poll::Ready(()) => {
                        // Future 完成,不再放回隊列
                    }
                }
            }
        }
    }
}

/**
 * # 用生活白話解釋 Rust 中的 await
 * 
 * 想像你去麥當勞點餐。傳統的同步(非 await)方式和使用 await 的異步方式有很大不同:
 * 
 * ## 同步方式(沒有 await)
 * 
 * 你去麥當勞點了一個漢堡:
 * 1. 你站在櫃檯前,告訴店員你要一個漢堡
 * 2. 然後你就站在那裡**一動不動地等待**
 * 3. 其他人想點餐都不行,因為你擋住了櫃檯
 * 4. 店員做好漢堡後,終於把漢堡遞給你
 * 5. 你拿到漢堡,這時其他人才能點餐
 * 
 * 這就像程式中的阻塞調用 - 整個系統(櫃檯)被你占用,無法處理其他事情。
 * 
 * ## 異步方式(使用 await)
 * 
 * 你去麥當勞點了一個漢堡,但使用了"取餐號碼牌"系統:
 * 1. 你告訴店員你要一個漢堡
 * 2. 店員給你一個號碼牌,說:"漢堡還沒好,你先去旁邊坐著吧,好了會叫你"
 * 3. 你拿著號碼牌去旁邊坐下(這就是 `await` 的時刻)
 * 4. **此時櫃檯空出來了**,其他人可以上前點餐
 * 5. 你可以玩手機、看書或聊天 - 做些其他事情
 * 6. 廣播叫到你的號碼時(相當於 `waker.wake()`),你回到櫃檯
 * 7. 拿到你的漢堡後,你才繼續後面的活動(買飲料、找座位等)
 * 
 * 這就是 `await` 的精髓:
 * - **讓出資源**:當你的漢堡還沒準備好時,你不是傻站在櫃檯前,而是先去坐著,讓櫃檯可以服務其他人
 * - **保留上下文**:系統記住你點了什麼、付了多少錢(你的程式狀態)
 * - **通知恢復**:漢堡做好時會通知你(wake 機制)
 * - **繼續執行**:拿到漢堡後,你可以繼續你的用餐計劃(代碼的後續部分)
 * 
 * ## 再具體一點的例子
 * 
 * `let result = simple_async_function().await;` 就像:
 * "我要點一個特殊漢堡(調用 simple_async_function)然後等它好了(await)才繼續點飲料"
 * 
 * 而在底層,系統不是讓你站在那傻等,而是:
 * 1. 記錄下你當前的狀態("這人要特殊漢堡,之後想點飲料")
 * 2. 給你一個號碼牌,讓你先坐著
 * 3. 當漢堡好了,叫你的號碼
 * 4. 你回來拿漢堡,然後繼續點飲料
 * 
 * 這就是為什麼 `await` 這麼神奇 - 它讓程式能高效利用等待時間做其他事情,而不是傻傻地阻塞在那裡。
 */

fn main() {
    println!("程序開始");
    
    let executor = SimpleExecutor::new();
    
    // 把我們的異步函數封裝並發送到執行器
    executor.spawn(async {
        let result = simple_async_function().await;
        println!("最終結果: {}", result);
    });
    
    // 運行執行器,處理所有任務
    executor.run();
    
    println!("程序結束");
}

Rust 生命週期省略規則總整理 & 最簡單範例

什麼是生命週期省略規則?

Rust 編譯器會根據「省略規則」自動推斷引用的生命週期,讓你大部分情況不用手動標註 'a。但遇到複雜或不明確的情境,還是需要你明確標註。


省略規則可省略的情況

  1. 只有一個輸入引用參數,回傳引用
    編譯器自動把回傳值生命週期綁到輸入參數。

    #![allow(unused)]
    fn main() {
    fn get_first(s: &str) -> &str {
        &s[0..1]
    }
    }
  2. 方法只有 &self 或 &mut self,回傳引用
    編譯器自動綁定到 self。

    #![allow(unused)]
    fn main() {
    struct User { name: String }
    impl User {
        fn get_name(&self) -> &str {
            &self.name
        }
    }
    }

需要手動標註的情況

  1. 多個輸入引用參數,回傳引用
    編譯器無法判斷回傳引用屬於哪個參數。

    #![allow(unused)]
    fn main() {
    // 錯誤範例
    // fn longest(a: &str, b: &str) -> &str { ... }
    
    // 正確範例
    fn longest(a: &'a str, b: &'a str) -> &'a str {
        if a.len() > b.len() { a } else { b }
    }
    }
  2. 結構體/枚舉含有引用成員
    必須標註生命週期參數。

    #![allow(unused)]
    fn main() {
    // 錯誤範例
    // struct Book { title: &str }
    
    // 正確範例
    struct Book {
        title: &'a str,
    }
    }
  3. 無輸入引用但回傳引用
    編譯器無法推斷,需明確標註(通常是 'static)。

    #![allow(unused)]
    fn main() {
    // 錯誤範例
    // fn get_str() -> &str { "hello" }
    
    // 正確範例
    fn get_str() -> &'static str {
        "hello"
    }
    }
  4. 閉包或函數指針涉及引用
    通常要用 for 明確標註。

    #![allow(unused)]
    fn main() {
    // 正確範例
    let closure: Box Fn(&'a str) -> &'a str> = Box::new(|s| s);
    }

最簡單的生命週期標註範例

#![allow(unused)]
fn main() {
// 兩個輸入引用,回傳其中一個,需要標註
fn pick_first(a: &'a str, b: &'a str) -> &'a str {
    a
}
}

小結

  • 簡單情況(單一引用、self 方法):編譯器自動推斷。
  • 複雜情況(多引用、結構體含引用、無輸入回傳引用、閉包):必須手動標註。
  • 原則:只要回傳值的生命週期無法自動推斷,就要手動標註。

這樣整理後,遇到錯誤訊息時就能快速判斷是否需要加上 'a

Rust 基本教學

Hello World!

不使用管理工具編寫程式

  1. 建立副檔名為 .rs 的檔案 ex: main.rs
  2. 寫 main function,程式碼編譯過後,會以 main function 作為進入點
fn main () {
    
}
  1. 印出 Hello World!
fn main () {
    println!("Hello World");
}
  1. 編譯程式碼
rustc main.rs
  1. 編譯完後會產生 main/main.exe 檔,執行 main/main.exe
./main

使用 Cargo 管理專案

  1. 建立專案
cargo new hello_world
  1. 編輯 src/main.rs 的檔案
fn main() {
    println!("Hello, world!");
}
  1. 檢查專案是否可以編譯得過
cargo check
  1. 編譯
cargo build
  1. 執行
cargo run
  1. 優化編譯
cargo build --release

Hello World 的程式碼解析

fn main () {} // fn 是 function 的關鍵字
println!("Hello World!") // println 是印出資料的語法,! 是 macro 的寫法, println!  是官方的 macro ,會在編譯時期根據目標平臺轉換成相應的程式碼

宣告變數

  • 在 rust 中,可以不用宣告型別,也會由編譯器自行推定
fn main () {
    let x = 5; // 會被自行推定為 i32
    let y: i32 = 10; // 也可以宣告變數型別
}
  • 預設所有變數都是不可變的
fn main () {
    let x = 5;
    x = 10; // 不可以改變 x 的值
}
  • 若要改變變數,必須宣告 mut
fn main () {
    let mut x = 5;
    x = 10; // OK
}
  • 可以用 tuple 或 struct 的方式宣告多個變數並同時賦值
fn main () {
    let (a, b) = (1, 2);
    let (mut x, mut y) = (1, 2); // 或宣告可變的變數
}
  • 可以事先宣告變數,但若變數被宣告後沒有初始化,同時在之後被使用到,會編譯不過
fn main () {
    let x: i32;
    println!("{}", x); // use of possibly-uninitialized `x`
}
fn main () {
    let x: i32;
    let condition = true;
    if condition {
        x = 1; // 因為在這裡被初始化了
        println!("{}", x); // 所以可以使用
    }
    // 但在這裡沒有被初始化
    println!("{}", x); // 在這裡會出錯
}
  • 有的時候會在接別人的 API 時,遇到用不到,但必須寫出來的變數,可以用底線 帶過,就可以讓編譯器閉嘴,讓編譯器忽略沒有使用到這個變數,但同時底線變數也被視為不能被使用的變數,所以不可以在後面的程式碼中使用到
fn main () {
    let _ = "hello";
    println!("{}", _); // expected expression
}
  • 如果在寫程式的途中,想要命名一個跟前面名稱一模一樣的變數是可以的
fn main () {
    let x = "Hello";
    println!("{}", x);
    
    let x = 5; // 前面的變數會被 shadowing
    println!("{}", x);
}
  • 可以用 type 為一個型別取新的名字
#![allow(unused)]
fn main() {
type Age = u32;
fn grow (age: Age, year: u32) -> Age {
    age + year
}
}
  • 宣告靜態變數
static GLOBAL: i32 = 0;
  • 宣告常數
const GLOBAL: i32 = 0;

型別種類

  • bool
  • char
  • 數字
  • array
#![allow(unused)]
fn main() {
let a = [1, 2, 3];
let first = a[0];
let second = a[1];
}
  • tuple
#![allow(unused)]
fn main() {
let a = ("hello", 1)
}
  • struct
struct Person {
    age: u32,
    weight: u32,
}

fn main () {
    let ballfish = Person { age: 18, weight: 40 };
    println!("{}, {}", age, weight);
}
  • tuple struct
#![allow(unused)]
fn main() {
struct Color (i32, i32, i32);
}
  • enum
#![allow(unused)]
fn main() {
enum Food {
    Noodle,
    Rice
}

enum Message {
    Quit,
    ChangeColor(i32, i32, i32),
    Move { x: i32, y: i32 },
}
}

if/else、loop、function

  • 用大括號括起來的區塊,可以放在等號後面,最後一行不寫分號,會被視為回傳直傳出去
fn main () {
    let x = { println!("喵喵喵喵"); 5 };
    println!("{}", x);
}
  • if/else
fn main () {
    let n = 4;
    if n < 0 {
        println!("Wow");
    } else if n == 0 {
        println!("owo");
    } else {
        println!("Orz");
    }
    // Orz
}
  • Rust 並沒有三元運算子(ex: n < 0 ? “owo” : “OAO”),但可以把 if/else 寫成下面這樣
fn main () {
    let n = 4;
    let x = if n < 4 { "owo" } else { "OAO" };
    println!("{}", x);
    // OAO
}
  • Rust 中,若 else 沒有寫出來,視為回傳 (),以剛剛的例子而言,由於 x 必須在編譯時期就確定型態,所以 if/else 回傳的值必須一致。也因此,通常 if 會伴隨 else,除非 if 沒有回傳值
fn main () {
    let n = 4;
    let x = if n < 4 { "owo" } else { 5 }; // expected `&str`, found integer
    println!("{}", x);
}
fn main () {
    let n = 4;
    let x = if n < 4 { "owo" }; // expected `()`, found `&str`
}
fn main () {
    let n = 4;
    let x = if n < 5 { println!("OAO") };
    // OAO
}
  • loop,是一個不帶條件的無限迴圈
fn main () {
    loop {
        print!("喵喵喵喵");
    }
}
  • 跟其他的程式語言一樣,Rust 有 continuebreak
fn main () {
    loop {
        print!("汪汪");
        break;
    }
    
    loop {
        print!("喵喵喵喵");
        continue;
        print!("噗伊");
    }
}
  • 你可以在 break 後面接值,這個值會被作為回傳值
fn main () {
    let a = loop {
        println!("噗伊");
        break 5;
    };
    println!("{}", a);
    // 噗伊
    // 5
}
  • while,帶有條件的迴圈
fn main () {
    let mut n = 0;
    while n < 10 {
        println!("喵喵喵喵");
        n += 1;
    }
}
  • while 也可以接在等號後面,但因為 breakwhile 中不能接值,所以永遠會回傳 ()
fn main () {
    let mut n = 0;
    let x = while n < 10 {
        n += 1;
        break 5; // can only break with a value inside `loop` or breakable block
    };
}
fn main () {
    let mut n = 0;
    // OK,但 x = ()
    let x = while n < 10 {
        n += 1;
        break;
    };
}
  • loop 與 while 的差異
    • loop 是不帶條件一定會執行的迴圈
    • while 是帶有條件,不一定會被執行的迴圈
    • 對於編義器來說,while block 裡的程式碼不一定會被執行到,無論 while 後面接的是不是 true
    • 也是因為這個差異,breakwhile 裡面才會不能接值,因為 while 沒有被執行的情況下,回傳直永遠是 (),所以在 while block 裡的回傳值一定要是 ()
fn main () {
    let x;
    loop { x = 1; break; }
    println!("{}", x); // x 一定會在 loop 中被初始化,所以編譯會過
}
fn main () {
    let x;
    while true { x = 1; break; }
    // 因為編譯器無法保證 while block 裡的程式碼一定會被執行到,所以無法保證 x 一定會被初始化,因此編譯不會過
    println!("{}", x); // use of possibly-uninitialized `x`
}
  • for loop,Rust 中沒有其他語言常有的 for (i = 0;i < 10;i++),Rust 中的 for loop形式跟其他語言的 for-each 視同義的
fn main () {
    let array = [1, 2, 3];
    for i in array.iter() {
        println!("{}", i);
    }
}
  • for loop 跟 while 的特性一樣,block 中的 break 後不可以接值, for loop 可以接在等號後面
  • function
#![allow(unused)]
fn main() {
fn add (t: (i32, i32)) -> i32 {
    t.0 + t.1
}
}
  • function 的 parameter 還可以直接解構
#![allow(unused)]
fn main() {
fn add ((x, y): (i32, i32)) -> i32 {
    x + y
}
}
  • function 可以作為一個普通的變數
fn add ((x, y): (i32, i32)) -> i32 {
    x + y
}
fn main () {
    let func = add;
    println!("{}", func((1, 2))); // 3
}
  • 不會正常回傳的 function
#![allow(unused)]
fn main() {
fn amazing () -> ! {
    panic!("crash the application~~");
}
}
  • const fn,加上 const 關鍵字的 function 可以在編譯時期被執行,執行完的值也可以作為常數使用
const fn add ((x, y): (i32, i32)) -> i32 {
    x + y
}

fn main () {
    const CONSTANT: i32 = add((1, 2));
    println!("{}", CONSTANT);
}
  • Rust 的 function 可以遞迴,但跟其他語言一樣過多層的遞迴會 stack overflow

Rust 所有權系統

所有權的機制 (Ownership)

  1. 所有的 Value 與 記憶體位置 只會有一個 變數 管理他們,意思是不會有兩個變數同時紀錄同一個記憶體位置
  2. 所有的 Value 與 記憶體位置 都必須要有個一個 變數 管理他們,所以當變數因為生命週期結束時,代表Value會被銷毀、記憶體位置會被釋放

所有權轉移 (Move)

img

#[derive(Debug)]
struct Person {
    age: i32
}
fn main () {
    let x = Person { age: 16 };
    let y = x;
    // borrow of moved value: `x`
    // 所有權被轉移了,所以你不能再使用 x
    println!("{:?}", x);
    println!("{:?}", y);
}

按位複製 (Copy)

img

  • 記憶體位置是不能被 Copy 的
  • struct 在沒有實現 Copy 前,是不會進行 Copy,而會進行 Move
  • 但 array、tuple、Option 本身就有實現 Copy,所以在所有的值都可以實現 Copy 的情況,會進行 Copy,如果有一個值不能實現 Copy 則會進行 Move
  • 實現 Copy、Clone trait (因為 Copy 繼承 Clone,所以必須同時實現 Copy 與 Clone trait) (關於 trait 會在之後的章節提到)
#[derive(Debug)]
struct Person {
    age: i32
}
// Clone trait 用來實現 deep clone
// 任何類型都可以實作 Clone
impl Clone for Person {
    fn clone (&self) -> Person {
        Person { age: self.age }
    }
}
// Copy trait 像是一個標籤
// 他裡面沒有任何可以實現的 function
// 但實作 Copy 的 struct 可以進行 Copy
// 不過可以實作 Copy 的 struct,成員必須不包含指標類型
impl Copy for Person {}
fn main () {
    let x = Person { age: 16 };
    let y = x;
    println!("{:p}", &x);
    println!("{:?}", x);
    println!("{:p}", &y);
    println!("{:?}", y);
}
  • 快速實現 Copy 與 Clone
#[derive(Debug, Copy, Clone)]
struct Person {
    age: i32
}
fn main () {
    let x = Person { age: 16 };
    let y = x;
    println!("{:p}", &x);
    println!("{:?}", x);
    println!("{:p}", &y);
    println!("{:?}", y);
}

所有權借用 (Borrow)

介紹

  • 借用分成不可變借用(&)跟可變借用(&mut)
  • 用 & 來借用
#[derive(Debug)]
struct Person {
    age: i32
}
fn birthday (y: &mut Person) {
    y.age = y.age + 1;
}
fn main () {
    let mut x = Person { age: 16 };
    birthday(&mut x);
    println!("{:?}", x);
}
  • 沒有借用的情況,所有權會被轉移
#[derive(Debug)]
struct Person {
    age: i32
}
fn birthday (mut y: Person) {
    y.age = y.age + 1;
}
fn main () {
    let x = Person { age: 16 };
    birthday(x);
    println!("{:?}", x);
}

output

#![allow(unused)]
fn main() {
   |
9  |     let x = Person { age: 16 };
   |         - move occurs because `x` has type `Person`, which does not implement the `Copy` trait
10 |     birthday(x);
   |              - value moved here
11 |     println!("{:?}", x);
   |                      ^ value borrowed here after move
}

借用的規則 (Rust 核心原則之一:共享不可變,可變不共享)

  • 在不可變借用期間 (共享),擁有者不能修改 Value,也不能進行可變借用 (不可變),但可以再進行不可變借用
#[derive(Debug)]
struct Person {
    age: i32
}
#[allow(dead_code)]
fn birthday (y: &mut Person) {
    y.age = y.age + 1;
}
#[allow(unused_mut)]
fn main () {
    let mut x = Person { age: 16 };
    let y = &x; // 不可變借用,擁有者是 x,借用者是 y
    println!("{:p}", &x); // x 可以再進行不可變借用
    // cannot borrow `x` as mutable because it is also borrowed as immutable
    // 但不可以再進行可變借用
    birthday(&mut x);
    println!("{:?}", y); // 借用者 y 可以使用 x,印出值
}
  • 在可變借用期間 (可變),擁有者不能存取 Value,也不能進行不可變借用 (不共享)
#[derive(Debug)]
struct Person {
    age: i32
}
#[allow(dead_code)]
fn birthday (y: &mut Person) {
    y.age = y.age + 1;
}
#[allow(unused_mut)]
fn main () {
    let mut x = Person { age: 16 };
    let y = &mut x; // 可變借用,擁有者是 x,借用者是 y
    y.age = 1;
    // cannot borrow `x` as immutable because it is also borrowed as mutable
    // x 不可以再進行不可變借用
    println!("{:p}", &x);
    // cannot borrow `x` as mutable more than once at a time
    // 當然也不可以再進行可變借用
    birthday(&mut x);
    // cannot use `x.age` because it was mutably borrowed
    // 同時你也不可以存取 x
    y.age = x.age + 1;
    println!("{:?}", y); // 借用者 y 可以使用 x,印出值
}
  • 借用者的

    生命週期

    不能夠長於擁有者

    • 範例在生命週期的章節再寫

Also see

  • https://shihyu.github.io/rust_hacks/ch6/02_move_copy.html
  • https://rust-lang.tw/book-tw/ch04-01-what-is-ownership.html

Rust-所有權

https://ithelp.ithome.com.tw/articles/10272643

所有權可以說是Rust核心概念,這讓Rust不需要垃圾回收(garbage collector)就可以保障記憶體安全。Rust的安全性和所有權的概念息息相,因此理解Rust中的所有權如何運作是非常重要的

所有權的規則

  • Rust 中每個數值都會有一個變數作為它的擁有者(owner)
  • 同時間只能有一個擁有者。
  • 當擁有者離開作用域時,數值就會被丟棄。

變數作用域

用下面這段程序描述變數範圍的概念

#![allow(unused)]
fn main() {
{
    // 在宣告以前,變數s無效
    let s = "hello";
    // 這裡是變數s的可用範圍
}
// 變數範圍已經結束,變數s無效
}

變數作用域是變數的一個屬性,其代表變數的可使用範圍,默認從宣告變數開始有效直到變數所在作用域結束。

記憶體與分配

定義一個變數並賦予值,這個變數的值存在記憶體中,例如需要用戶輸入的一串字串由於長度的不確定只能存放在堆(heap)上,這需要記憶體分配器在執行時請求記憶體並在不需要時還給分配器

在擁有垃圾回收機制(garbage collector, GC)的語言中,GC會追蹤並清除不再使用的記憶體,如果沒有GC的話則需要在不使用時顯式的呼叫釋放記憶體

例如C語言

#![allow(unused)]
fn main() {
{    
    char *s = strdup("hello");    
    free(s); *// 釋放s資源*
}
}

Rust選擇了一個不同的道路,當變數在離開作用域時會自動釋放例如下面

#![allow(unused)]
fn main() {
{
    let s = String::from("hello"); // s 在此開始視為有效
    // 使用 s
} // 此作用域結束,釋放s變數     
}

當變數離開作用域(大括號結束)時會自動呼叫特殊函示drop來釋放記憶體

變數與資料互動的方式

移動(Move)

變數可以在Rust中以不同的方式與相同的資料進行互動

#![allow(unused)]
fn main() {
let x = 100;
let y = x;
}

這個代碼將值100綁定到變數x,然後將x的值復制並賦值給變數y現在棧(stack)中將有兩個值100。此情況中的數據是"純量型別"的資料,不需要存儲到堆中,僅在棧(stack)中的資料的"移動"方式是直接複製,這不會花費更長的時間或更多的存儲空間。"純量型別"有這些:

  • 所有整數類型,例如 i32 、 u32 、 i64 等
  • 布爾類型 bool,值為true或false
  • 所有浮點類型,f32和f64
  • 字符類型 char
  • 僅包含以上類型數據的元組(Tuples)

現在來看一下非純量型別的移動

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1;
}

String物件的值"hello"為不固定長度長度型別所以被分配到堆(heap)

當s1賦值給s2,String的資料會被拷貝,不過我們拷貝是指標、長度和容量。我們不會拷貝指標指向的資料

前面説當變數超出作用域時,Rust自動調用釋放資源函數並清理該變數的記憶體。但是s1和s2都被釋放的話堆(heap)區中的"hello"被釋放兩次,這是不被系統允許的。為了確保安全,在給s2賦值時 s1已經無效了

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1; 
println!("{}, world!", s1); // 會發生錯誤 s1已經失效了
}

克隆(clone)

正常情況下Rust在較大資料上都會以淺拷貝的方式,當然也有提供深拷貝的method

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{} {}", s1, s2);

輸出
hello hello
}

所有權與函式

將一個變數當作函式的參數傳給其他函式,怎樣安全的處理所有權

傳遞數值給函式這樣的語義和賦值給變數是類似的。傳遞變數給函式會是移動或拷貝就像賦值一樣

fn main() {
    // s被宣告
    let s = String::from("hello"); // s進入作用域

    takes_ownership(s); // s的值被當作參數傳入函式 所以可以當作s已經被移動,從這開始已經無效

    // x被宣告
    let x = 5; // x進入作用域

    makes_copy(x); // x的值被當作參數傳入函式,但x是純量型別 i32被copy,依然有效
} // 函式結束,x無效,接著是s的值已經被移動了它不會有任何動作

fn takes_ownership(some_string: String) {
    // 一個String參數some_string傳入,有效
    println!("{}", some_string);
} // 函式結束,參數some_string佔用的記憶體被釋放

fn makes_copy(some_integer: i32) {
    // 一個i32參數some_integer傳入,有效
    println!("{}", some_integer);
} // 函式結束,參數some_integer是純量型別,沒有任何動作發生

如果在呼叫takes_ownership之後在使用s變數在編譯時會出錯

回傳值與作用域

回傳值轉移所有權

fn main() {
    let s1 = gives_ownership(); // gives_ownership移動它的回傳值給s1
    let s2 = String::from("哈囉"); // s2進入作用域
    let s3 = takes_and_gives_back(s2); // s2移入takes_and_gives_back,該函式又將其回傳值移到s3
} // s3 在此離開作用域並釋放
  // s2 已被移走,所以沒有任何動作發生
  // s1 離開作用域並釋放

// 此函式回傳一個String
fn gives_ownership() -> String {
    let some_string = String::from("hello"); // some_string進入作用域

    return some_string; // 回傳some_string並移動給呼叫它的函式
}

// 此函式會取得一個String然後回傳它
fn takes_and_gives_back(a_string: String) -> String {
    // a_string進入作用域
    return a_string; // 回傳a_string並移動給呼叫的函式
}

引用與借用在前面介紹定義函式時有介紹過了,這邊就不多講了

講一下迷途指標(dangling pointer),這個在很多指標語言常發生的錯誤

簡單講就是用到空指標,Rust會在編譯時檢查這類型的錯誤

例如

#![allow(unused)]
fn main() {
fn dangle() -> &String { // 回傳String的迷途引用
    let s = String::from("hello"); // 宣告一個新的String
    return &s // 回傳String的引用
} // s在此會離開作用域並釋放
}

編譯時會產生錯誤 missing lifetime specifier

Rust 生命週期 (Lifetime)

介紹(幹話)

  • 變數 從出生到死亡的時間段
fn main () {
    let x = Box::new(5); // x 出生
    println!("{:?}", x);
    {
        let y = Box::new(1); // y 出生
        println!("{:?}", y);
    } // y 死亡
    // cannot find value `y` in this scope
    // y 死掉了,所以你存取不到他
    println!("{:?}", y);
} // x 死亡

Borrow checker

  • 編譯器的機制
  • 會檢查借用者的生命週期會不會活的比擁有者久
  • 為了避免 null pointer 發生,就是擁有者已經死了,Value 已經被銷毀了,但借用者還活著,就會存取到不存在的東西
#[allow(unused_variables, unused_assignments)]
fn main () {
    let x; // x 出生
    {
        let y = Box::new(1); // y 出生
        x = &y; // x 借用 y 的所有權
        println!("{:?}", y);
    } // y 死亡
    // `y` does not live long enough
    // y 死掉了,所以 x 存取不到他 (1編譯器:可憐的 y 他活的不夠久 owo)
    println!("{:?}", x);
} // x 死亡

生命週期標示

  • 在名字前面加個 ' ,就是生命週期的標示
  • 以剛剛的例子來說
fn main () {
    test();
}

// 生命週期標示,必須像泛型一樣,在 function 簽名中先被宣告
fn test<'a, 'b> () {
    let x: &'a i32 = &5; // 'a 開始
    println!("{:?}", x);
    {
        let y: &'b i32 = &2; // 'b 開始
        println!("{:?}", y);
    } // 'b 結束
} // 'a 結束
  • 不必要標生命週期的情況
#[derive(Debug)]
struct Person {
    age: i32
}
// 因為 傳入值 與 回傳值 只有一個
// 不會造成編譯器需要檢查生命週期的問題
// 所以沒有必要標示生命週期
fn life_again_gun (y: &mut Person) -> &mut Person {
    y.age = 0;
    y
}
fn main () {
    let mut x = Person { age: 16 };
    let y = life_again_gun(&mut x);
    println!("{:?}", y);
}
  • 必須要標生命週期的情況
#[derive(Debug)]
struct Person {
    age: i32
}
// missing lifetime specifier
// 因為編譯器看不出回傳的 借用者 是不是會超過 擁有者 的 lifetime
// 所以要求你編上 lifetime
fn the_older (x: &Person, y: &Person) -> &Person {
    if x.age > y.age { x } else { y }
}
fn main () {
    
}
#[derive(Debug)]
struct Person {
    age: i32
}
// 我們預期這裡只會有一種生命週期
fn the_older<'a> (x: &'a Person, y: &'a Person) -> &'a Person {
    if x.age > y.age { x } else { y }
}
fn main () {
    let x = Person { age: 16 };
    let y = Person { age: 17 };
    let res = the_older(&x, &y);
    println!("{:?}", res)
}
  • 指定多個生命週期,並標示哪個生命週期比較長
#[derive(Debug)]
struct Person {
    age: i32
}
// 我們有兩個生命週期 'a 與 'b,其中 'b 活的比 'a 久
fn the_older<'a, 'b: 'a> (x: &'a Person, y: &'b Person) -> &'a Person {
    if x.age > y.age { x } else { y }
}
fn main () {
    let x = Person { age: 16 };
    let res;
    {
        let y = Person { age: 17 };
        res = the_older(&x, &y);
        println!("{:?}", res);
    }
}

NLL (Non-Lexical-Lifetime)

Lexical-Lifetime

  • 是指說生命週期與變數的作用域是綁定在一起的
  • 舉個例子
#[derive(Debug)]
struct Person {
    age: i32
}
fn birthday (y: &mut Person) {
    y.age = y.age + 1;
}
fn life_again_gun (y: &mut Person) -> &mut Person {
    y.age = 0;
    y
}
fn main () {
    let mut x = Person { age: 16 };
    let y = life_again_gun(&mut x);
    // 在 Lexical-Lifetime 的情況,y 的生命週期沒有結束
    // 所以 y 還在進行可變借用
    // 那理論上 x 就不可以再度可變出借
    // (NLL 好像已經是標準了,所以我無法實現 LL 的編譯錯誤)
    birthday(&mut x);
    println!("{:?}", x);
}

Non-Lexical-Lifetime

  • borrow checker 的分析結構方式從 AST 轉向 MIR
    • AST 是抽象語法樹,它會以樹狀的形式表現程式語言的語法結構,因為舊的 borrow checker 用 AST 做分析,所以會造成生命週期與作用域掛鉤
    • MIR 是中間表達式,他在編譯器內部會有像是流程圖的資料結構,用流程控制的方式去分析生命週期
    • 只要變數在後面的程式碼中,沒有機會被使用到,就會提早被結束生命週期
  • NLL 將作用域與生命週期拆開來看了
  • NLL 縮短了過長的生命週期 (縮減了變數的生命),讓程式不會充滿一堆 block 去迴避 LL 造成的問題
  • 舉例來說
#[derive(Debug)]
struct Person {
    age: i32
}
fn birthday (y: &mut Person) {
    y.age = y.age + 1;
}
fn life_again_gun (y: &mut Person) -> &mut Person {
    y.age = 0;
    y
}
fn main () {
    let mut x = Person { age: 16 };
    let y = life_again_gun(&mut x);
    // 在 Non-Lexical-Lifetime 的情況
    // y 在這段程式碼的後面都沒有被使用到
    // y 的生命週期就結束了
    // 那這裡就不會有問題
    birthday(&mut x);
    println!("{:?}", x);
}
#[derive(Debug)]
struct Person {
    age: i32
}
fn birthday (y: &mut Person) {
    y.age = y.age + 1;
}
fn life_again_gun (y: &mut Person) -> &mut Person {
    y.age = 0;
    y
}
fn main () {
    let mut x = Person { age: 16 };
    let y = life_again_gun(&mut x);
    // cannot borrow `x` as mutable more than once at a time
    // 但如果 y 在後面有機會被使用到
    // 就代表 y 的生命週期還沒有結束
    // 所以 x 不可以再度進行可變出借
    birthday(&mut x);
    y.age = 16;
    println!("{:?}", x);
}

Borrow 的存活時間

出處: https://ithelp.ithome.com.tw/articles/10200106

Rust 有個重要的功能叫 borrow checker ,它除了檢查在上一篇提到的規則外,還檢查使用者會不會使用到懸空參照 (dangling reference) ,懸空參照是在電腦世界中一種現象: 如果你今天把一個變數借給別人,實際上借走的人只是知道我可以去哪裡找到這個別人借我的東西而已,那個東西的擁有者還是你本人,以現實世界做比喻的話,這像是借別人東西只是把放那個東西的儲物櫃位置,以及鑰匙暫時的交給別人而已,送別人東西則是直接把儲物櫃的擁有者變成他。

所以如果今天發生了一種情況,你把東西借給別人後,管理每個儲物櫃擁有者的系統馬上把你的使用權收回去呢?會發生什麼事,這沒人說的準,可能儲物櫃還沒被清空,你還是可以拿到借來的東西,或是馬上又換了主人,你已經不是拿到原本的東西了,就像以下的程式碼:

#![allow(unused)]
fn main() {
fn foo() ->&i32 {
  // 這個變數在離開這個範圍後就消失了
  let a = 42;
  // 但是這邊卻回傳了 borrow
  &a
}
}

上面這段 code 是無法編譯的。

為瞭解決這樣的一個問題, Rust 提出來的就是 lifetime 的觀念,只要函式的參數或回傳值有 borrow 出現,使用者就要幫 borrow 標上 lifetime ,標記後讓編譯器可以去追蹤每個變數借出去與釋放掉的情況,確保不會有釋放掉已經出借的變數的可能性。

Rust 使用 'a 一個單引號加上一個識別字當作 lifetime 的標記,所以這些都是可以的 'b, 'foo, '_bar ,此外有兩個保留用作特殊用途的 lifetime: 'static'_

  • 'static: 這代表這是個整個程式都有效的 borrow 比如字串常數 "foo" 它的 lifetime 就是 'static
  • '_:這是保留給 Rust 2018 使用的,這裡先不提它的功能

這邊是個加上 lifetime 標記後的範例:

#![allow(unused)]
fn main() {
fn foo<'a>(a: &'a i32) -> &'a i32 {
  a
}
}

其中我們必須在函式名稱後加上 <> 並在其中宣告我們的 lifetime ,接著把 borrow 的 & 後都加上我們的 lifetime 標記,但事實上在上一篇文章中,我們完全沒用使用到 lifetime , Rust 可以在某些情況下自動推導出正確的 lifetime ,使得實際上需要手動標註的情況並不多,最有可能遇到的情況是一個函式同時使用了兩個 borrow :

fn max<'a>(a: &'a i32, b: &'a i32) -> &'a i32 {
  if a > b {
    a
  } else {
    b
  }
}

fn main() {
  let a = 3;
  let m = &a;
  {
    let b = 2;
    let n = &b;
    // 對於 max 來說, m 與 n 同時存活的這個範圍就是 'a ,
    // 而回傳值也可以在這個範圍內使用
    println!("{}", max(m, n));
  } // b 與 n 會在這邊消失
} // a 與 m 會在這邊消失

這種情況編譯器因為看到了兩個 borrow ,於是沒辦法猜出來回傳的值應該要跟哪個 lifetime 一樣,這邊的作法就是全部都標記一樣的 lifetime ,讓 Rust 知道說我們的變數都會存活在同一個範圍內,同時回傳值也可以在同樣的範圍存活。

大部份的情況下編譯器都能自動的推導,所以需要手動標註的情況其實不多,通常是先嘗試讓編譯器做推導,如果編譯器報錯了才來想辦法標註。

lifetime 還有個用途是用來限制使用者傳入的參數必須是常數:

#![allow(unused)]
fn main() {
fn print_message(message: &'static str) {
  println!("{}", message);
}
}

這個函式就只能接受如 "Hello" 這樣的常數了,雖說只是偶爾會有這樣的需求。

Lifetime Elision (Lifetime 省略規則) (進階)

這部份大概的瞭解一下就好了

  1. 所有的 borrow 都會自動的分配一個 lifetime
#![allow(unused)]
fn main() {
fn foo(a: &i32, b: &i32);
fn foo<'a, 'b>(a: &'a i32, b: &'b i32); // 推導結果
}
  1. 如果函式只有一個 borrow 的參數,則它的 lifetime 會自動被應用到回傳值上
#![allow(unused)]
fn main() {
fn foo(a: &i32);
fn foo<'a>(a: &'a i32) -> &'a i32; // 推導結果
}
  1. 如果有多個 borrow ,但其中一個是 self ,則 self 的 lifetime 會被應用在回傳值
#![allow(unused)]
fn main() {
impl Foo {
  fn method(&self, a: &i32) -> &Self {
  }
}

// 推導結果
impl Foo {
  fn method<'a, 'b>(&'a self, b: &'b i32) -> &'a Self {
  }
}
}

若不符合上面任一條規則,則必須要標註型態。

如果我們把以上的規則套用在上面的範例 max 上:

#![allow(unused)]
fn main() {
fn max(a: &i32, b: &i32) -> &i32 {
  if a > b {
    a
  } else {
    b
  }
}
}

套用規則 1 :

#![allow(unused)]
fn main() {
fn max<'a, 'b>(a: &'a, i32, b: &'b i32) -> &i32 {
  if a > b {
    a
  } else {
    b
  }
}
}

到這邊結束,編譯器已經沒有可用的規則了,但是回傳值的 lifetime 依然是未知,於是就編譯失敗。


https://buckychu.im/2022/10/05/rust-lifetime/

Rust 的生命週期

今天我們要來介紹其他程式語言中比較少見的機制,但是在 Rust 中是屬於和參考(reference)有關的機制,那就是生命週期(lifetime)。

一、生命週期的概念

生命週期是 Rust 中的一個概念,它是一個變數的有效範圍,也就是它可以被使用的範圍。生命週期的概念是為瞭解決 Rust 中的參考的問題,因為參考是 Rust 中的一個重要機制,它可以讓開發者在不需要複製資料的情況下,就可以使用資料。但是參考也有一個問題,就是它的生命週期,也就是它的有效範圍,如果參考的資料已經被釋放了,那麼參考就會變成一個無效的參考,這樣就會造成程式的錯誤。

這裡有一個範例:

#![allow(unused)]
fn main() {
{
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}
}

先說結論,這個範例是沒辦法通過編譯的,也就是會報錯。 這是因為我們先宣告了一個變數 r,然後我們在一個新的區塊中宣告了一個變數 x,並且將 x 的參考賦值給 r。 這個時候,x 的生命週期就結束了,但是 r 仍然使用 x 的參考,這樣就會造成程式的錯誤。

二、生命週期的標記

  • 生命週期的標記不會改變參考的生命週期,它只是用來標記參考的生命週期,讓 Rust 編譯器可以知道這個參考的生命週期是多少。
  • 當指定了泛型生命週期參數後,函式就可以接收帶有任何生命週期的參考。

在語法上有以下幾個重點:

  • 生命週期參數名稱:
    • 以單引號 ' 開頭
    • 一般以全部小寫字母命名
    • 大部分的慣例都使用 'a 作為生命週期參數名稱
  • 生命週期標記的位置:
    • 在參考的 & 符號之後
    • 使用空格將生命週期與參考分開

看一下以下的範例:

#![allow(unused)]
fn main() {
RUST
&i32        // 一個參考
&'a i32     // 一個有顯式生命週期的參考
&'a mut i32 // 一個有顯式生命週期的可變參考
}

單一個生命週期標記是沒有意義的,這是因為標記生命週期是為了要讓 Rust 編譯器知道多個參考的生命週期之間的關係,所以如果只有一個參考,那麼就沒有必要標記生命週期。

以下是一個範例:

#![allow(unused)]
fn main() {
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s2.len() > s1.len() {
        s2
    } else {
        s1
    }
}
}

這個函式接收兩個參考,並且回傳一個參考,這個函式的生命週期參數名稱是 'a,這個生命週期參數名稱被用在了函式的參數與回傳值上,這樣就可以讓 Rust 編譯器知道這三個參考的生命週期是相同的。

最後在執行這個函式的時候就不會問題,並且可以正常執行:

fn main() {
    let r;
    {
        let s1 = "rust";
        let s2 = "ecmascript";
        r = longer(s1, s2);
        println!("{} is longer", r); // ecmascript is longer
    }
}

三、生命週期的規則

在 Rust 中,生命週期的規則有三個:

  1. 每個參考都有一個生命週期
  2. 每個參考都有一個作用域
  3. 一個參考的生命週期不能超過它的作用域

總結

Rust 的生命週期跟所有權,兩者在其語言中的資源管理機制上都是非常重要的。由於參考是 Rust 在對於複雜類型中不可少的機制,而每個參考都有其生命週期,這是為了決定該參考是否有效的作用域。

前面有提到 Rust 的型別大多數其實都可以自動判別,而生命週期其實也一樣,都是可以自動推導出來。不過當生命週期以不同方式互相牽連的狀態下,開發者就要自行設定,這也跟型別非常複雜的狀態下,開發者就要自行設定型別一樣。

以上就是 Rust 的生命週期的基本概念,希望大家對 Rust 又更瞭解了一些。


fn example<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where 'b: 'a // 'b lives at least as long as 'a  添加了 where 'b: 'a 約束,確保 'b 的生命週期至少與 'a 一樣長
{
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let x = String::from("hello");
    let y = String::from("world");

    let x_ref: &str = &x;
    let y_ref: &str = &y;

    let result = example(x_ref, y_ref);
    println!("Result: {}", result);

    println!("x content address: {:p}", x.as_ptr());
    println!("y content address: {:p}", y.as_ptr());
    println!("result content address: {:p}", result.as_ptr());
}

Rust 型別系統

型別大小

  • Sized Tyep
    • 大部分的類型都是 Sized Type,就是可以在編譯時期就知道大小的
    • 例如:u32, i64
  • Dynamic Sized Type
    • 無法在編譯時期知道大小的型別則叫作「DST (Dynamic Sized Type)」
    • 例如:[T], Box
    • img
  • Zero Sized Type
    • 另外還有一種類型叫「ZST (Zero Sized Type)」,在執行時期,不佔用空間大小的型別
    • 你可以用 ZST 來做一些反覆運算,Rust 編譯器有對 ZST 做最佳化
fn main() {
    let v: Vec<()> = vec![(); 10];
    // 像是你可以這樣寫
    for _i in v {
        println!("{:?}", 1);
    }
    
    // 雖然你有更簡單的寫法
    for _i in 1..10 {
        println!("{:?}", 1);
    }
}
  • Bottom Type
    • 只的是 never 類型
    • 程式碼中用 ! 表示
    • 特點
      • 沒有值
      • 是任意類型的子類型
    • Bottom Type 的用處
      • Diverging Function (發散函數)
      • loop 迴圈
      • 空列舉 enum Void{}
    • ex:
fn print_meow_forever () -> ! {
    loop { println!("meow"); }
}

fn main () {
    let i = if false {
        print_meow_forever();
    } else {
        100
    };
    println!("{}", i);
}
  • turbofish
    

    運算子

    • 用來做顯示的型別宣告 ex:
fn main () {
    let x = "1";
    println!("{}", x.parse::<i32>().unwrap());
}

泛型

  • 用這樣的語法<T>宣告泛型 ex:
#![allow(unused)]
fn main() {
struct Point<T> { x: T, y: T } 
}

trait 用法

宣告 interface

  • interface 裡可以定義 function 或 type
  • interface 裡不能實作另一個 interface,但 interface 之間可以繼承
  • 使用 impl 實作 interface
  • 使用 trait 宣告 interface
  • 孤兒原則 (Orphan Rule)
    • 要實現某個 trait,這個 trait 必須要在當前的 crate 中被定義
    • 用來避免標準函式庫,或在其他地方被定義好的 trait 被修改到,而難以追查

實作自己的 Add:

trait Add<RHS, Output> {
    fn my_add (self, rhs: RHS) -> Output;
}

impl Add<i32, i32> for i32 {
    fn my_add (self, rhs: i32) -> i32 {
        self + rhs
    }
}

impl Add<u32, i32> for u32 {
    fn my_add (self, rhs: u32) -> i32 {
        (self + rhs) as i32
    }
}

fn main () {
    let (a, b, c, d) = (1i32, 2i32, 3u32, 4u32);
    let x: i32 = a.my_add(b);
    let y: i32 = c.my_add(d);
    println!("{}", x);
    println!("{}", y);
}

標準函式庫裡的 Add trait

#![allow(unused)]
fn main() {
pub trait Add<RHS = Self> {
    type Output;
    fn add (self, rhs: RHS) -> Self::Output;
}
}

標準函式庫 u32 的加法實作

#![allow(unused)]
fn main() {
impl Add for u32 {
    type Output = u32;
    fn add (self, rhs: u32) -> u32 { self + rhs }
}
}

標準函式庫 String 的加法實作

#![allow(unused)]
fn main() {
impl Add for String {
    type Output = String;
    fn add (mut self, rhs: &str) -> String {
        self.push_str(rhs);
        self
    }
}
}

trait 裡的 function 可以有一個 default 的實作

trait Top {
    fn wear_top (&mut self, _clothes: String) {
        println!("Default: coat");
    }
}

trait Bottom {
    fn wear_bottom (&mut self, _clothes: String) {
        println!("Default: pants");
    } 
}

struct PersonLikeCoat {
    top: String,
    bottom: String,
}

impl Top for PersonLikeCoat {}

impl Bottom for PersonLikeCoat {
    fn wear_bottom (&mut self, clothes: String) {
        self.bottom = clothes;
        println!("Changed: {}", self.bottom);
    }
}

fn main () {
    let mut ballfish = PersonLikeCoat { top: String::from("coat"), bottom: String::from("pants") };
    ballfish.wear_top(String::from("sweater"));
    ballfish.wear_bottom(String::from("skirt"));
}

trait 的繼承

trait Top {
    fn wear_top (&mut self, _clothes: String) {
        println!("Default: coat");
    }
}

trait Bottom {
    fn wear_bottom (&mut self, _clothes: String) {
        println!("Default: pants");
    } 
}

struct Person {
    top: String,
    bottom: String,
}

impl Top for Person {}

impl Bottom for Person {
    fn wear_bottom (&mut self, clothes: String) {
        self.bottom = clothes;
        println!("Changed: {}", self.bottom);
    }
}

trait WholeBody: Top + Bottom {
    fn wear_whole_body (&mut self, top: String, bottom: String) {
        self.wear_top(top);
        self.wear_bottom(bottom);
    }
}

impl WholeBody for Person {}

fn main () {
    let mut ballfish = Person { top: String::from("coat"), bottom: String::from("pants") };
    ballfish.wear_whole_body(String::from("sweater"), String::from("skirt"));
}

用 trait 對泛型做限定 (trait Bound)

  • img

語法 fn generic<T: FirstTrait + SecondTrait>(t: T) {}fn generice<T> (t: T) where T: FirstTrait + SecondTrait {}

ex:

trait Top {
    fn wear_top (&mut self, _clothes: String) {
        println!("Default: coat");
    }
}

trait Bottom {
    fn wear_bottom (&mut self, _clothes: String) {
        println!("Default: pants");
    } 
}

struct Person {
    top: String,
    bottom: String,
}

impl Top for Person {}

impl Bottom for Person {
    fn wear_bottom (&mut self, clothes: String) {
        self.bottom = clothes;
        println!("Changed: {}", self.bottom);
    }
}

fn go_routin1<P: Top + Bottom> (p: &mut P) {
    p.wear_top(String::from("sweater"));
    p.wear_bottom(String::from("skirt"));
}

fn go_routin2<P> (p: &mut P) where P: Top + Bottom {
    p.wear_top(String::from("sweater"));
    p.wear_bottom(String::from("skirt"));
}

fn main () {
    let mut ballfish = Person { top: String::from("coat"), bottom: String::from("pants") };
    go_routin1::<Person>(&mut ballfish); // ::<Person> 可省
    go_routin2::<Person>(&mut ballfish); // ::<Person> 可省
}

宣告抽象型別 (Abstract Type)

  • Abstract Type 是無法產生實體的型別
  • rust 有兩種方式處理抽象型別:trait Object、impl Trait
  • trait Object
    • 將 trait 當作一種型別使用
    • 與 trait bound 有點像,但 trait bound 是靜態分配,而 trait Object 是動態分配
    • trait Object 在編譯時期無法知道其記憶體大小,所以他本身是一種指標

img

pub struct TraitObject {
    pub data: *mut (),
    pub vtable: *mut (),
}
  • 上面的 struct 來自標準函式庫,但不是真的 trait 物件

  • data指標指向trait物件儲存的類型資料T

  • vtable指標指向包含為T實作的virtual table (虛表)

  • 虛表本身是一種struct,包含解構函數、大小、方法等

  • 編譯器只知道trait object的指標,但不知道要呼叫哪個方法

  • 運行期, 會從虛表中查出正確的指標• 再進行動態呼叫

  • Trait物件的限制

    • Trait的Self有一個隱式的trait bound ?Sized<Self: ?Sized>,包含所有可確定大小的類型,也就是<T: Sized>
    • 但trait物件的Self不能被限定是Sized,因為trait物件一定是動態分配,所以不可能滿足Sized的條件
    • 但Trait物件在運行期進行動態分發時必須確定大小,否則無法為其正確分配記憶體空間
    • 因此trait中的方法必定是物件安全,物件安全即為必受到Self: Sized的約束,且為沒有額外Self類型參數的非泛型方法
    #![allow(unused)]
    fn main() {
    // 物件不安全的 trait
    trait Foo {
        fn bad<T> (&self, x: T);
        fn new() -> Self;
    }
    
    // 方法一:將不安全的部份拆出去
    trait Bar {
        fn bad<T> (&self, x: T);
    }
    trait Foo: Bar {
        fn new() -> Self;
    }
    
    // 方法二:使用 where
    trait Foo {
        fn bad<T>(&self, x: T);
        fn new() -> Self where self: Sized; // 但這個 trait 作為物件時, new 會無法被呼叫
    }
    }

動態分配與靜態分配的比較

trait Top {
    fn wear_top (&mut self, _clothes: String) {
        println!("Default: coat");
    }
}

trait Bottom {
    fn wear_bottom (&mut self, _clothes: String) {
        println!("Default: pants");
    } 
}

struct Person {
    top: String,
    bottom: String,
}

impl Top for Person {}

impl Bottom for Person {
    fn wear_bottom (&mut self, clothes: String) {
        self.bottom = clothes;
        println!("Changed: {}", self.bottom);
    }
}

trait WholeBody: Top + Bottom {
    fn wear_whole_body (&mut self, top: String, bottom: String) {
        self.wear_top(top);
        self.wear_bottom(bottom);
    }
}

impl WholeBody for Person {}

fn static_dispatch<P: WholeBody> (p: &mut P) {
    p.wear_top(String::from("sweater"));
    p.wear_bottom(String::from("skirt"));
}

fn dynamic_dispatch (p: &mut WholeBody) {
    p.wear_top(String::from("sweater"));
    p.wear_bottom(String::from("skirt"));
}

fn main () {
    let mut ballfish = Person { top: String::from("coat"), bottom: String::from("pants") };
    static_dispatch::<Person>(&mut ballfish); // ::<Person> 可省
    dynamic_dispatch(&mut ballfish);
}

impl Trait

  • 是靜態分配的抽象類型
trait Fly {
    fn fly(&self) -> bool;
}

struct Duck;

impl Fly for Duck {
    fn fly(&self) -> bool {
        return true;
    }
}

fn fly_static (s: impl Fly) -> bool {
    s.fly()
}

fn can_fly (s: impl Fly) -> impl Fly {
    if s.fly() {
        println!("fly!");
    } else {
        println!("fell!")
    }
    s // return s
}

fn main () {
    let duck = can_fly(Duck);
}
  • 雖然這個語法很有趣,但有些情況編譯器會誤判,例如下面的例子,ab,被編譯器認定為不同的 type,所以 sum 會報錯
#![allow(unused)]
fn main() {
use std::ops::Add;
fn sum<T>(a: impl Add<Output=T>, b: impl Add<Output=T>) -> T {
    a + b
}
}
  • 與靜態分配型態相對的是 dyn Trait 動態分配的型態
#![allow(unused)]
fn main() {
fn dyn_can_fly (s: impl Fly+'static) -> Box<dyn Fly> {
    if s.fly() {
        println!("fly!");
    } else {
        println!("fell!");
    }
    Box::new(s)
}
}

標籖

  • Rust一共提供5個常用的標籖• 被定義在

    #![allow(unused)]
    fn main() {
    std::marker
    }

    裡e

    • Sized 用來標識編譯期可確定大小的類型,大部份類型都預設定義實作 Sized
    • Unsize 用來標識動態大小類型
    • Copy 用來標識可安全按位複製類型
    • Send 用來標識可跨執行緒安全傳遞值的類型,也就是可以跨執行緒傳遞所有權
    • Sync 用來標識可在執行緒間安全共用參考的類型
  • 標籤類 trait,都是用下面這種寫法標示他的標籤性質

#![allow(unused)]
fn main() {
#[lang = "sized"] // lang 表示 Sized trait 供 Rust 語言本身使用
pub trait Sized {} // 此程式為空,無實作方法
}

類型轉換

Deref

  • 參考使用&
  • 設定值使用*
  • 可以實作Deref的trait來自訂設定值的操作
  • Deref是強制轉型的,如果某個類型T實作Deref<Target=U>,則使用T的參考時,參考會被轉型成U
fn foo (s: &[i32]) {
    println!("{:?}", s[0]);
}

fn main () {
    let a = "hello".to_string();
    let b = " world".to_string();
    // b 被自動 deref
    let c = a + &b;
    println!("{:?}", c);

    /// &Vec<T> -> &[T]
    let v = vec![1, 2, 3];
    foo(&v);

    let x = Rc::new("hello");
    let y = x.clone(); // Rc<&str>
    // 如果想要呼叫 &str 的 clone,必須要自己 deref
    let z = (*x).clone(); // &str
}

as 運算符號

類型轉換(含生命週期)

fn main () {
    let a = 1u32;
    let b = a as u64;
    println!("{:?}", a);
    println!("{:?}", b);
    let c = std::u32::MAX;
    let d = c as u16;
    println!("{:?}", c);
    println!("{:?}", d);
    let e = -1i32;
    let f = e as u32;
    println!("{:?}", e);
    println!("{:?}", f);
    let a: &'static str = "hello"; // &'static str
    let b: &str = a as &str;
    let c: &'static str = b as &'static str;
}

限定用法

struct S(i32);
trait A {
    fn test(&self, i: i32);
}

trait B {
    fn test(&self, i: i32);
}

impl A for S {
    fn test(&self, i: i32) {
        println!("From A: {:?}", i);
    }
}

impl B for S {
    fn test(&self, i: i32) {
        println!("From B: {:?}", i)
    }
}

fn main () {
    let s = S(1);
    A::test(&s, 2);
    B::test(&s, 3);
    <S as A>::test(&s, 4);
    <S as B>::test(&s, 5);
}

From與Into

  • 定義於std::convert
  • 互為反向操作
#[derive(Debug)]
struct Person { name: String }
impl Person {
    fn new<T: Into<String>>(name: T) -> Person {
        Person { name: name.into() }
    }
}

fn main () {
    let person = Person::new("Alex");
    let person = Person::new("Alex".to_string());
    println!("{:?}", person);

    // String from 的方法
    let to_string = "hello".to_string();
    let from_string = String::from("hello");
    assert_eq!(to_string, from_string);

    // 如果 U 實現了 From<T>,則 T 類型的實例,都可以呼叫 into 方法轉換為 U
    let a = "hello";
    let b: String = a.into();
    // 所以一般情況只要實作 From 即可,除非 From 很難實作,才需要實作 Into
}

Trait系統的不足

孤兒原則

  • 孤兒原則解說
  • 若下游程式想要使用擴充某些 crate,就必須包裝成新的 type,以迴避孤兒原則
  • 而對一些本地端的類型,在被 Option,或是 Rc 等 interface 包裝後,就會被認定為非本地端類型,擴充時就會發生問題
use std::ops::Add;
#[derive(PartialEq)]
struct Int(i32);
impl Add<i32> for Int {
    type Output = i32;
    fn add (self, other: i32) -> Self::Output {
        (self.0) + other
    }
}

impl Add<i32> for Option<Int> {} // (X)

// 因為 Rust 裡 Box 有 #[fundamental] 標籤
impl Add<i32> for Box<Int> {
    type Output = i32;
    fn add (self, other: i32) -> Self::Output {
        (self.0) + other
    }
}

fn main () {
    assert_eq!(Int(3) + 3, 6);
    assert_eq!(Box::new(Int(3)) + 3, 6);
}

程式複用率不高

  • 重複原則

    • 規定不可以為重疊的類型實作同一個 trait
    #![allow(unused)]
    fn main() {
    impl<T> AnyTrait for T {}
    impl<T> AnyTrait for T where T: Copy {}
    impl<T> AnyTrait for i32 {}
    }
#![allow(unused)]
fn main() {
//  效能問題
// 這裡實作了 += 的對應方法
impl<R, T: Add<R> + Clone> AddAssign<R> for T {
    fn add_assign(&mut self, rhs: R) {
        // clone 會造成效能的負擔,有些類型不需要用 clone 這個方法
        // 但因為重複原則,無法限縮實作對象,所以為了效能,很多作法是位每個類型實作 trait
        // 造成程式複用度不高
        let tmp = self.clone() + rhs;
        *self = tmp;
    }
}
}

你應該知道的7個Rust Cargo外掛

1. Cargo watch cargo watch監視你的項目原始檔,並在原始檔更改時運行Cargo命令。 安裝命令如下:

cargo install cargo-watch

例子如下:

fn main() {
    println!("Hello, world!");
    println!("Hello, cargo-watch!");
}

然後,我們可以在命令列輸入以下命令,以便在原始檔更改時執行cargo run:

cargo watch -x run

輸出如下:

cargo watch -x run
[Running 'cargo run']
Hello, world!
Hello, cargo-watch!
[Finished running. Exit status: 0]

如果我們註釋掉第2行,我們的程式碼將自動重新編譯。

[Running 'cargo run']
Hello, cargo-watch!
[Finished running. Exit status: 0]

cargo watch也提供了很多其他的選項,如:

cargo watch -c -q -w ./src -x run

-c 來清空終端 -q 抑制cargo watch本身的輸出 -w 關注某個目錄,這裡只關注src目錄。 -x 運行cargo命令

2. Cargo edit cargo edit 允許你從命令列新增、升級和刪除依賴項。 安裝命令如下:

sudo apt-get install librust-openssl-dev
cargo install cargo-edit

我們在命令列輸入以下命令,將rand 0.7版本新增到項目中:

cargo add rand@0.7

這將自動更新Cargo.toml檔案:

img

如圖所示,rang 0.7不是最新版本,我們可以使用以下命令來升級依賴項:

cargo upgrade --incompatible

結果如下:

cargo upgrade --incompatible     
    Updating 'https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index' index
    Checking cargo-plugin-example's dependencies
name old req compatible latest new req
==== ======= ========== ====== =======
rand 0.7     0.7.3      0.8.5  0.8    
   Upgrading recursive dependencies

我們還可以通過以下命令來刪除依賴項:

cargo rm rand

3. Cargo modules cargo modules外掛允許我們可視化項目的模組結構,以樹狀格式顯示模組結構。

安裝命令如下:

#![allow(unused)]
fn main() {
cargo install cargo-modules
}

在命令列輸入以下命令:

cargo modules generate tree

輸出結果:

crate cargo_plugin_example
├── mod modules: pub(crate)
└── mod utils: pub(crate)

在這裡,我們可以看到默認的crate模組,兩個頂級模組models和utils,我們還可以看到每個模組的可見性。

通過新增types,fns標誌,我們也可以看到每個模組內部的類型,函數等。

cargo modules generate tree --types --fns

結果如下:

crate cargo_plugin_example
├── fn main: pub(crate)
├── mod modules: pub(crate)
│   └── struct Message: pub
└── mod utils: pub(crate)
    └── fn msg_helpers: pub

4. Cargo audit cargo audit檢查項目的依賴項是否有任何安全漏洞,這在持續整合中特別有用。

安裝命令如下:

cargo install cargo-audit

要審計你的項目,只需輸入如下命令:

cargo audit

結果如下:

Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
      Loaded 578 security advisories (from /Users/Justin/.cargo/advisory-db)
    Updating crates.io index
    Scanning Cargo.lock for vulnerabilities (9 crate dependencies)

在我們的項目中,沒有發現任何漏洞。

5. Cargo tarpaulin cargo tarpaulin 是另一個對持續整合非常有用的外掛,這個外掛計算項目的程式碼覆蓋率。 安裝命令如下:

cargo install cargo-tarpaulin

在項目根目錄下,輸入如下命令:

cargo tarpaulin

結果如下:

|| Uncovered Lines:
|| src/main.rs: 6-9
|| src/utils.rs: 1
|| Tested/Total Lines:
|| src/main.rs: 0/4 +0.00%
|| src/utils.rs: 0/1 +0.00%
|| 
0.00% coverage, 0/5 lines covered, +0.00% change in coverage

6. Cargo nextest cargo-nextest 是新一代的rust測試程序,它提供了漂亮的測試結果,片狀的測試檢測,並且在某些程式碼庫上可以將測試運行速度提高60倍。 安裝命令如下:

cargo install cargo-nextest

要使用cargo-nextest執行測試,需要在命令列執行如下命令:

cargo nextest run

結果如下: img 正如你所看到的,輸出是有組織的,並且具有漂亮的顏色,這有助於提高可讀性。

7. Cargo make cargo-make 是rust的任務運行器和建構工具,它允許你定義一組任務並在流程中運行它們,任務可以在toml檔案中定義。

安裝命令如下:

cargo install cargo-make

在項目根目錄下建立build.toml檔案,內容如下:

[tasks.format]
command = "cargo"
args = ["fmt", "--", "--emit=files"]
[tasks.clean]
command = "cargo"
args = ["clean"]
[tasks.build]
command = "cargo"
args = ["build"]
dependencies = ["clean"]
[tasks.test]
command = "cargo"
args = ["nextest", "run"]
dependencies = ["clean"]
[tasks.build-flow]
dependencies = [
    "format",
    "build",
    "test"
]

它包括幾個任務:格式化程式碼,清理程式碼,建構程式碼,測試程式碼。最後定義一個建構流程,按流程給定順序執行任務。

在命令列執行如下命令執行cargo make:

cargo make --makefile build.toml build-flow

結果如下: img 至此,你應該知道的7個Rust Cargo外掛已經介紹完了,希望對你有所幫助。


rustup update # バージョン更新
rustup component add rustfmt # フォーマッタ追加
rustup component add rust-src # LSP(rust-analyzer)追加
rustup component add rust-analyzer

在 Rust 中使用 Cargo Watch 實時監聽代碼變化

當我們開發 Rust 應用程序時,有時我們需要減少更改、編譯和執行週期的時間。這聽起來有點複雜,但今天我將向你展示一個工具,它以一種非常簡單和自動的方式來實時監聽代碼變化。這個工具被稱爲 cargo watch,它減少了項目變更、編譯、運行週期的時間。

Cargo Watch

Cargo Watch 在你的項目上創建了一個監聽器,並在發生更改時運行 Cargo 命令。

img

我們從創建一個新項目開始。對於我們的例子,我們將其稱爲 cargo-watch-example:

cargo new cargo-watch-example

安裝 cargo watch:

cargo install cargo-watch

運行項目並觀察變化:

cargo watch -x run

如果希望只從工作目錄監聽變化,則添加 - w 選項以指定要從其中監聽變化更改的文件或目錄:

cargo watch -w src -x run

例如:

img

前面的例子是觀察整個項目或特定目錄上的變化的最簡單的配置,但你可以做的事情更多,這裏是命令說明:

USAGE:
    cargo watch [FLAGS] [OPTIONS]

FLAGS:
    -c, --clear              Clear the screen before each run
    -h, --help               Display this message
        --ignore-nothing     Ignore nothing, not even target/ and .git/
        --debug              Show debug output
        --why                Show paths that changed
    -q, --quiet              Suppress output from cargo-watch itself
        --no-gitignore       Don’t use .gitignore files
        --no-ignore          Don’t use .ignore files
        --no-restart         Don’t restart command while it’s still running
        --poll               Force use of polling for file changes
        --postpone           Postpone first run until a file changes
    -V, --version            Display version information
        --watch-when-idle    Ignore events emitted while the commands run.
                             Will become default behaviour in 8.0.

OPTIONS:
    -x, --exec <cmd>...
            Cargo command(s) to execute on changes [default: check]

    -s, --shell <cmd>...           Shell command(s) to execute on changes

    -d, --delay <delay>
            File updates debounce delay in seconds [default: 0.5]

        --features <features>
            List of features passed to cargo invocations

    -i, --ignore <pattern>...      Ignore a glob/gitignore-style pattern

    -B <rust-backtrace>
            Inject RUST_BACKTRACE=VALUE (generally you want to set it to 1)
            into the environment

        --use-shell <use-shell>
            Use a different shell. E.g. --use-shell=bash. On Windows, try
            --use-shell=powershell, which will become the default in 8.0.

    -w, --watch <watch>...
            Watch specific file(s) or folder(s) [default: .]

    -C, --workdir <workdir>
            Change working directory before running command [default: crate root]

ARGS:
    <cmd:trail>...    Full command to run. -x and -s will be ignored!

Cargo commands (-x) are always executed before shell commands (-s). You can use
the `-- command` style instead, note you'll need to use full commands, it won't
prefix `cargo` for you.

By default, your entire project is watched, except for the target/ and .git/
folders, and your .ignore and .gitignore files are used to filter paths.

On Windows, patterns given to -i have forward slashes (/) automatically
converted to backward ones (\) to ease command portability.

join with Dataframe

use polars::df;
use polars::prelude::*;

fn join_test() -> Result<DataFrame, PolarsError> {
    let df1: DataFrame = df!("Wavelength (nm)" => &[480.0, 650.0, 577.0, 1201.0, 100.0])?;
    let df2: DataFrame = df!("Color" => &["Blue", "Yellow", "Red"],
                         "Wavelength nm" => &[480.0, 577.0, 650.0])?;

    let df3: DataFrame = df1.left_join(&df2, ["Wavelength (nm)"], ["Wavelength nm"])?;
    // println!("{:?}", df3);
    Ok(df3)
}

fn main() {
    let df = join_test();
    println!("{:#?}", df)
    // match join_test() {
    //     Ok(df) => println!("DataFrame: {:#?}", df),
    //     Err(e) => println!("Error: {}", e),
    // }
}
[package]
name = "polars_test"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
polars = { version = "0.27.2", features = ["json"] }

Call C dynamic library in rust

前言

c語言作為系統編程語言統治bit世界已經很久,留下了大量的代碼遺產。rust作為新興語言在一些冷門領域開發,真是裹足前行。rust如果可以調用c,那真是再好不過。

一、初始化rust工程

如果是vim寫代碼的用戶,可以直接使用,如果是ide,自行創建工程。

cargo new --bin test_rust_call_c

二、生成一個c動態庫

如果瞭解在c裡面生成動態庫的流程可不看,這個使用簡單的add函數(返回兩個入參的和),演示流程,至於更多的類型轉化可看官方文檔。

1.add.h

#ifndef _ADD_H
#ifdef __cplusplus
extern "C" {
#endif
int add(int a, int b);
#ifdef __cplusplus
}
#endif
#endif

2.add.c

#include "add.h"
int add(int a, int b) {
    return a + b;
}

3.add.so

gcc -fPIC -shared add.c -o libadd.so

三、在rust裡面調用動態庫

1.main.rs內容

現在開始在rust調用c。這裡需要告訴rust編譯器,c函數原型,使用 extern "C" 包裹下。 使用c函數的地方必須用unsafe塊包裹,默認編譯器使用很嚴格的檢查標準,加上unsafe塊編譯器會把檢查權利讓給開發人員自己。

extern "C" {
    fn add(a: i32, b: i32) -> i32;
}

fn main() {
    unsafe {
        println!("{}", add(1, 2));
    }
}

2.編譯

這裡面要告訴rust編譯器要鏈接的動態庫是誰,-l add 會自動補齊然後找libadd.so的文件。-L path。下面的例子是在當前目錄下面找。

rustc src/main.rs -l add -L .

3.運行

運行時也要通過LD_LIBRARY_PATH告知動態庫的位置。剩下的就是運行。

env LD_LIBRARY_PATH=. ./main

四、優化工程,更符合rust的方式

使用build.rs編譯,和三、2同樣的效果

這裡對上面的編譯方式做些優化,在rust裡面一般是編寫build.rs,生成依賴,以後在生成protobuf或者grpc代碼還可以看到類似套路。

// build.rs rust的編譯腳本
fn main() {
    println!("cargo:rustc-link-search=."); // 等於rustc -L .
    println!("cargo:rustc-link-lib=dylib=add"); // 等於rustc -ladd
}

參考資料

https://doc.rust-lang.org/cargo/reference/build-scripts.html

https://zhuanlan.zhihu.com/p/70095462

http://liufuyang.github.io/2020/02/02/call-c-in-rust.html

解密 Python 如何呼叫 Rust 編譯生成的動態連結庫

楔子

Rust 讓 Python 更加偉大,隨著 Rust 的流行,反而讓 Python 的生產力提高了不少。因為有越來越多的 Python 工具,都選擇了 Rust 進行開發,並且性能也優於同類型的其它工具。比如:

  • ruff:速度極快的程式碼分析工具,以及程式碼格式化工具;
  • orjson:一個高性能的 JSON 解析庫;
  • watchfiles:可以對指定目錄進行即時監控;
  • polars:和 pandas 類似的資料分析工具;
  • pydantic:資料驗證工具;
  • ......

總之現在 Rust + Python 已經成為了一個趨勢,並且 Rust 也提供了一系列成熟好用的工具,比如 PyO3、Maturin,專門為 Python 編寫擴展。不過關於 PyO3 我們以後再聊,本篇文章先來介紹如何將 Rust 程式碼編譯成動態庫,然後交給 Python 的 ctypes 模組呼叫。

因為通過 ctypes 呼叫動態庫是最簡單的一種方式,它只對作業系統有要求,只要作業系統一致,那麼任何提供了 ctypes 模組的 Python 直譯器都可以呼叫。

當然這也側面要求,Rust 提供的介面不能太複雜,因為 ctypes 提供的互動能力還是比較有限的,最明顯的問題就是不同語言的資料類型不同,一些複雜的互動方式還是比較難做到的,還有多執行緒的控制問題等等。

之前說過使用 ctypes 呼叫 C 的動態庫,裡面詳細介紹了 ctypes 的用法,因此本文關於 ctypes 就不做詳細介紹了。

舉個例子

下面我們舉個例子感受一下 Python 和 Rust 的互動過程,首先通過如下命令建立一個 Rust 項目:

複製

cargo new py_lib --lib1.

建立完之後修改 Cargo.toml,在裡面加入如下內容:

複製

[lib]
# 編譯之後的動態庫的名稱
name = "py_lib"
# 表示編譯成一個和 C 語言二進制介面(ABI)相容的動態連結庫
crate-type = ["cdylib"]1.2.3.4.5.

cdylib 表示生成動態庫,如果想生成靜態庫,那麼就指定為 staticlib。

下面開始編寫原始碼,在生成項目之後,src 目錄下會有一個 lib.rs,它是整個庫的入口點。我們的程式碼比較簡單,直接寫在 lib.rs 裡面即可。

複製

#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn get_square_root(v: i32) -> f64 {
    (v as f64).sqrt()
}1.2.3.4.5.6.7.8.9.

在定義函數時需要使用 pub extern "C" 進行聲明,它表示建立一個外部可見、遵循 C 語言呼叫約定的函數,因為 Python 使用的是 C ABI。

此外還要給函數新增一個 #[no_mangle] 屬性,讓編譯器在將 Rust 函數匯出為 C 函數時,不要改變函數的名稱。確保在編譯成動態庫後,函數名保持不變,否則在呼叫動態庫時就找不到指定的函數了。

Rust 有個名稱修飾(Name Mangling)的機制,在跨語言操作時,會修改函數名,增加一些額外資訊。這種修改對 Rust 內部使用沒有影響,但會干擾其它語言的呼叫,因此需要通過 #[no_mangle] 將該機制停用掉。

程式碼編寫完成,我們通過 cargo build 進行編譯,然後在 target/debug 目錄下就會生成相應的動態庫。由於庫的名稱我們指定為 py_lib,那麼生成的庫檔案名稱就叫 libpy_lib.dylib。

當功能全部實現並且測試通過時,最好重新編譯一次,並加上 --release 參數。這樣可以對程式碼進行最佳化,當然編譯時間也會稍微長一些,並且生成的庫檔案會在 target/release 目錄中。

編譯器生成動態庫後,會自動加上一個 lib 前綴(Windows 系統除外),至於後綴則與作業系統有關。

  • Windows 系統,後綴名為 .dll;
  • macOS 系統,後綴名為 .dylib;
  • Linux 系統,後綴名為 .so;

然後我們通過 Python 進行呼叫。

複製

import ctypes

# 使用 ctypes 很簡單,直接 import 進來
# 然後使用 ctypes.CDLL 這個類來載入動態連結庫
# 或者使用 ctypes.cdll.LoadLibrary 也是可以的
py_lib = ctypes.CDLL("../py_lib/target/debug/libpy_lib.dylib")

# 載入之後就得到了動態連結庫對象,我們起名為 py_lib
# 然後通過屬性訪問的方式去呼叫裡面的函數
print(py_lib.add(11, 22))
"""
33
"""

# 如果不確定函數是否存在,那麼建議使用反射
# 因為函數不存在,通過 . 的方式獲取是會拋異常的
get_square_root = getattr(py_lib, "get_square_root", None)
if get_square_root:
    print(get_square_root)
    """
    <_FuncPtr object at 0x7fae30a2b040>
    """

# 不存在 sub 函數,所以得到的結果為 None
sub = getattr(py_lib, "sub", None)
print(sub)
"""
None
"""1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.

所以使用 ctypes 去呼叫動態連結庫非常方便,過程很簡單:

  • 1)通過 ctypes.CDLL 去載入動態庫;
  • 2)載入動態連結庫之後會返回一個對象,我們上面起名為 py_lib;
  • 3)然後直接通過 py_lib 呼叫裡面的函數,但為了程序的健壯性,建議使用反射,確定呼叫的函數存在後才會呼叫;

我們以上就演示瞭如何通過 ctypes 模組來呼叫 Rust 編譯生成的動態庫,但顯然目前還是遠遠不夠的,比如說:

複製

from ctypes import CDLL

py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")

square_root = py_lib.get_square_root(100)
print(square_root)  # 01.2.3.4.5.6.

100 的平方根是 10,但卻返回了 0。這是因為 ctypes 在解析返回值的時候默認是按照整型來解析的,但當前的函數返回的是浮點型,因此函數在呼叫之前需要顯式地指定其返回值類型。

不過在這之前,我們需要先來看看 Python 類型和 Rust 類型之間的轉換關係。

數值類型

使用 ctypes 呼叫動態連結庫,主要是呼叫庫裡面使用 Rust 編寫好的函數,但這些函數是需要參數的,還有返回值。而不同語言的變數類型不同,Python 不能直接往 Rust 編寫的函數中傳參,因此 ctypes 提供了大量的類,幫我們將 Python 的類型轉成 Rust 的類型。

與其說轉成 Rust 的類型,倒不如說轉成 C 的類型,因為 Rust 匯出的函數要遵循 C 的呼叫約定。

下面來測試一下,首先編寫 Rust 程式碼:

複製

#[no_mangle]
pub extern "C" fn add_u32(a: u32) -> u32 {
    a + 1
}
#[no_mangle]
pub extern "C" fn add_isize(a: isize) -> isize {
    a + 1
}
#[no_mangle]
pub extern "C" fn add_f32(a: f32) -> f32 {
    a + 1.
}
#[no_mangle]
pub extern "C" fn add_f64(a: f64) -> f64 {
    a + 1.
}
#[no_mangle]
pub extern "C" fn reverse_bool(a: bool) -> bool {
    !a
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.

編譯之後 Python 進行呼叫。

複製

from ctypes import *

py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")

print(py_lib.add_u32(123))
"""
124
"""
print(py_lib.add_isize(666))
"""
667
"""
try:
    print(py_lib.add_f32(3.14))
except Exception as e:
    print(e)
"""
<class 'TypeError'>: Don't know how to convert parameter 1
"""
# 我們看到報錯了,告訴我們不知道如何轉化第 1 個參數
# 因為 Python 的資料和 C 的資料不一樣,所以不能直接傳遞
# 但整數是個例外,除了整數,其它資料都需要使用 ctypes 包裝一下
# 另外整數最好也包裝一下,因為不同整數之間,精度也有區別
print(py_lib.add_f32(c_float(3.14)))
"""
1
"""
# 雖然沒報錯,但是結果不對,結果應該是 3.14 + 1 = 4.14,而不是 1
# 因為 ctypes 呼叫函數時默認使用整型來解析,但該函數返回的不是整型
# 需要告訴 ctypes,add_f32 函數返回的是 c_float,請按照 c_float 來解析
py_lib.add_f32.restype = c_float
print(py_lib.add_f32(c_float(3.14)))
"""
4.140000343322754
"""
# f32 和 f64 是不同的類型,佔用的位元組數也不一樣
# 所以 c_float 和 c_double 之間不可混用,雖然都是浮點數
py_lib.add_f64.restype = c_double
print(py_lib.add_f64(c_double(3.14)))
"""
4.140000000000001
"""

py_lib.reverse_bool.restype = c_bool
print(py_lib.reverse_bool(c_bool(True)))
print(py_lib.reverse_bool(c_bool(False)))
"""
False
True
"""1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.

不複雜,以上我們就實現了數值類型的傳遞。

字元類型

字元類型有兩種,一種是 ASCII 字元,本質上是個 u8;一種是 Unicode 字元,本質上是個 u32。

編寫 Rust 程式碼:

複製

#[no_mangle]
pub extern "C" fn get_char(a: u8) -> u8  {
    a + 1
}

#[no_mangle]
pub extern "C" fn get_unicode(a: u32) -> u32  {
    let chr = char::from_u32(a).unwrap();
    if chr == '憨' {
        '批' as u32
    } else {
        a
    }
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.

我們知道 Rust 專門提供了 4 個位元組 char 類型來表示 unicode 字元,但對於外部匯出函數來說,使用 char 是不安全的,所以直接使用 u8 和 u32 就行。

編譯之後,Python 呼叫:

複製

from ctypes import *

py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")

# u8 除了可以使用 c_byte 包裝之外,還可以使用 c_char
# 並且 c_byte 裡面只能接收整數,而 c_char 除了整數,還可以接收長度為 1 的位元組串
print(c_byte(97))
print(c_char(97))
print(c_char(b"a"))
"""
c_byte(97)
c_char(b'a')
c_char(b'a')
"""
# 以上三者是等價的,因為 char 說白了就是個 u8

# 指定返回值為 c_byte,會返回一個整數
py_lib.get_char.restype = c_byte
# c_byte(97)、c_char(97)、c_char(b"a") 都是等價的
# 因為它們本質上都是 u8,至於 97 也可以解析為 u8
print(py_lib.get_char(97))  # 98
# 指定返回值為 c_char,會返回一個字元(長度為 1 的 bytes 對象)
py_lib.get_char.restype = c_char
print(py_lib.get_char(97))  # b'b'


py_lib.get_unicode.restype = c_wchar
print(py_lib.get_unicode(c_wchar("嘿")))  # 嘿
# 直接傳一個 u32 整數也可以,因為 unicode 字元底層就是個 u32
print(py_lib.get_unicode(ord("憨")))  # 批1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.

以上就是字元類型的操作,比較簡單。

字串類型

再來看看字串,我們用 Rust 實現一個函數,它接收一個字串,然後返回大寫形式。

複製

use std::ffi::{CStr, CString};
use std::os::raw::c_char;

#[no_mangle]
pub extern "C" fn to_uppercase(s: *const c_char) -> *mut c_char {
    // 將 *const c_char 轉成 &CStr
    let s = unsafe {
        CStr::from_ptr(s)
    };
    // 將 &CStr 轉成 &str
    // 然後呼叫 to_uppercase 轉成大寫,得到 String
    let s = s.to_str().unwrap().to_uppercase();
    // 將 String 轉成 *mut char 返回
    CString::new(s).unwrap().into_raw()
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.

解釋一下里面的 CStr 和 CString,在 Rust 中,CString 用於建立 C 風格的字串(以 \0 結尾),擁有自己的記憶體。關鍵的是,CString 擁有值的所有權,當實例離開範疇時,它的解構函式會被呼叫,相關記憶體會被自動釋放。

而 CStr,它和 CString 之間的關係就像 str 和 String 的關係,所以 CStr 一般以引用的形式出現。並且 CStr 沒有 new 方法,不能直接建立,它需要通過 from_ptr 方法從原始指針轉化得到。

然後指針類型是 *const 和 *mut,分別表示指向 C 風格字串的首字元的不可變指針和可變指針,它們的區別主要在於指向的資料是否可以被修改。如果不需要修改,那麼使用 *const 會更安全一些。

我們編寫 Python 程式碼測試一下。

複製

from ctypes import *

py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
s = "hello 古明地覺".encode("utf-8")
# 默認是按照整型解析的,所以不指定返回值類型的話,會得到髒資料
print(py_lib.to_uppercase(c_char_p(s)))
"""
31916096
"""
# 指定返回值為 c_char_p,表示按照 char * 來解析
py_lib.to_uppercase.restype = c_char_p
print(
    py_lib.to_uppercase(c_char_p(s)).decode("utf-8")
)
"""
HELLO 古明地覺
"""1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.

從表面上看似乎挺順利的,但背後隱藏著記憶體洩露的風險,因為 Rust 裡面建立的 CString 還駐留在堆區,必須要將它釋放掉。所以我們還要寫一個函數,用於釋放字串。

複製

use std::ffi::{CStr, CString};
use std::os::raw::c_char;

#[no_mangle]
pub extern "C" fn to_uppercase(s: *const c_char) -> *mut c_char {
    let s = unsafe {
        CStr::from_ptr(s)
    };
    let s = s.to_str().unwrap().to_uppercase();
    CString::new(s).unwrap().into_raw()
}

#[no_mangle]
pub extern "C" fn free_cstring(s: *mut c_char) {
    unsafe {
        if s.is_null() { return }
        // 基於原始指針建立 CString,拿到堆區字串的所有權
        // 然後離開範疇,自動釋放
        CString::from_raw(s)
    };
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.

然後來看看 Python 如何呼叫:

複製

from ctypes import *

py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")

s = "hello 古明地覺".encode("utf-8")
# Rust 返回的是原始指針,這裡必須要拿到它保存的地址
# 所以指定返回值為 c_void_p,如果指定為 c_char_p,
# 那麼會直接轉成 bytes 對象,這樣地址就拿不到了
py_lib.to_uppercase.restype = c_void_p
# 拿到地址,此時的 ptr 是一個普通的整數,但它和指針保存的地址是一樣的
ptr = py_lib.to_uppercase(c_char_p(s))
# 將 ptr 轉成 c_char_p,獲取 value 屬性,即可得到具體的 bytes 對象
print(cast(ptr, c_char_p).value.decode("utf-8"))
"""
HELLO 古明地覺
"""
# 內容我們拿到了,但堆區的字串還沒有釋放,所以呼叫 free_cstring
py_lib.free_cstring(c_void_p(ptr))1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.

通過 CString 的 into_raw,可以基於 CString 建立原始指針 *mut,然後 Python 將指針指向的堆區資料複製一份,得到 bytes 對象。

但這個 CString 依舊駐留在堆區,所以 Python 不能將返回值指定為 c_char_p,因為它會直接建立 bytes 對象,這樣就拿不到指針了。因此將返回值指定為 c_void_p,呼叫函數會得到一串整數,這個整數就是指針保存的地址。

我們使用 cast 函數可以將地址轉成 c_char_p,獲取它的 value 屬性拿到具體的位元組串。再通過 c_void_p 建立原始指針交給 Rust,呼叫 CString 的 from_raw,可以基於 *mut 建立 CString,從而將所有權奪回來,然後離開範疇時釋放堆記憶體。

給函數傳遞指針

如果擴展函數裡面接收的是指針,那麼 Python 要怎麼傳遞呢?

複製

#[no_mangle]
pub extern "C" fn add(a: *mut i32, b: *mut i32) -> i32 {
    // 定義為 *mut,那麼可以修改指針指向的值,定義為 *const,則不能修改
    if a.is_null() || b.is_null() {
        0
    } else {
        let res = unsafe {
            *a + *b
        };
        unsafe {
            // 這裡將 *a 和 *b 給改掉
            *a = 666;
            *b = 777;
        }
        res
    }
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.

定義了一個 add 函數,接收兩個 i32 指針,返回解引用後相加的結果。但是在返回之前,我們將 *a 和 *b 的值也修改了。

複製

from ctypes import *

py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")

a = c_int(22)
b = c_int(33)
# 計算
print(py_lib.add(pointer(a), pointer(b)))  # 55
# 我們看到 a 和 b 也被修改了
print(a, a.value)  # c_int(666) 666
print(b, b.value)  # c_int(777) 7771.2.3.4.5.6.7.8.9.10.11.

非常簡單,那麼問題來了,能不能返回一個指針呢?答案是當然可以,只不過存在一些注意事項。

由於 Rust 本身的記憶體安全原則,直接從函數返回一個指向本地局部變數的指針是不安全的。因為該變數的範疇僅限於函數本身,一旦函數返回,該變數的記憶體就會被回收,從而出現懸空指針。

為了避免這種情況出現,我們應該在堆上分配記憶體,但這又出現了之前 CString 的問題。Python 在拿到值之後,堆記憶體依舊駐留在堆區。因此 Rust 如果想返回指針,那麼同時還要定義一個釋放函數。

複製

#[no_mangle]
pub extern "C" fn add(a: *const i32, b: *const i32) -> *mut i32 {
    // 返回值的類型是 *mut i32,所以 res 不能直接返回,因此它是 i32
    let res = unsafe {*a + *b};
    // 建立智能指針(將 res 裝箱),然後返回原始指針
    Box::into_raw(Box::new(res))
}

#[no_mangle]
pub extern "C" fn free_i32(ptr: *mut i32) {
    if !ptr.is_null() {
        // 轉成 Box<i32>,同時拿到所有權,在離開範疇時釋放堆記憶體
        unsafe { let _ = Box::from_raw(ptr); }
    }
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.

然後 Python 進行呼叫:

複製

from ctypes import *

py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")

a, b = c_int(22), c_int(33)
# 指定類型為 c_void_p
py_lib.add.restype = c_void_p
# 拿到指針保存的地址
ptr = py_lib.add(pointer(a), pointer(b))
# 將 c_void_p 轉成 POINTER(c_int) 類型,也就是 c_int *
# 通過它的 contents 屬性拿到具體的值
print(cast(ptr, POINTER(c_int)).contents)  # c_int(55)
print(cast(ptr, POINTER(c_int)).contents.value)  # 55
# 釋放堆記憶體
py_lib.free_i32(c_void_p(ptr))1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.

這樣我們就拿到了指針,並且也不會出現記憶體洩露。但是單獨定義一個釋放函數還是有些麻煩的,所以 Rust 自動提供了一個 free 函數,專門用於釋放堆記憶體。舉個例子:

複製

use std::ffi::{CStr, CString};
use std::os::raw::c_char;

#[no_mangle]
pub extern "C" fn to_uppercase(s: *const c_char) -> *mut c_char {
    let s = unsafe {
        CStr::from_ptr(s)
    };
    let s = s.to_str().unwrap().to_uppercase();
    CString::new(s).unwrap().into_raw()
}

#[no_mangle]
pub extern "C" fn add(a: *const i32, b: *const i32) -> *mut i32 {
    let res = unsafe {*a + *b};
    Box::into_raw(Box::new(res))
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.

這是出現過的兩個函數,它們的記憶體都申請在堆區,但我們將記憶體釋放函數刪掉了,因為 Rust 自動提供了一個 free 函數,專門用於堆記憶體的釋放。

複製

from ctypes import *

py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")

# 返回值類型指定為 c_void_p,表示萬能指針
py_lib.to_uppercase.restype = c_void_p
py_lib.add.restype = c_void_p

ptr1 = py_lib.to_uppercase(
    c_char_p("Serpen 老師".encode("utf-8"))
)
ptr2 = py_lib.add(
    pointer(c_int(123)), pointer(c_int(456))
)
# 函數呼叫完畢,將地址轉成具體的類型的指針
print(cast(ptr1, c_char_p).value.decode("utf-8"))
"""
SERPEN 老師
"""
print(cast(ptr2, POINTER(c_int)).contents.value)
"""
579
"""
# 釋放堆記憶體,直接呼叫 free 函數即可,非常方便
py_lib.free(c_void_p(ptr1))
py_lib.free(c_void_p(ptr2))1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.

以上我們就實現了指針的傳遞和返回,但對於整數、浮點數而言,直接返回它們的值即可,沒必要返回指針。

傳遞陣列

下面來看看如何傳遞陣列,由於陣列在作為參數傳遞的時候會退化為指針,所以陣列的長度資訊就丟失了,使用 sizeof 計算出來的結果就是一個指針的大小。因此將陣列作為參數傳遞的時候,應該將當前陣列的長度資訊也傳遞過去,否則可能會訪問非法的記憶體。

我們實現一個功能,Rust 接收一個 Python 陣列,進行原地排序。

複製

use std::slice;

#[no_mangle]
pub extern "C" fn sort_array(arr: *mut i32, len: usize) {
    assert!(!arr.is_null());

    unsafe {
        // 得到一個切片 &mut[i32]
        let slice = slice::from_raw_parts_mut(arr, len);
        slice.sort();  // 排序
    }
}1.2.3.4.5.6.7.8.9.10.11.12.

然後 Python 進行呼叫:

複製

from ctypes import *

py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")

# 一個列表
data = [3, 2, 1, 5, 4, 7, 6]
# 但是列表不能傳遞,必須要轉成 C 陣列
# Array_Type 就相當於 C 的 int array[len(data)]
Array_Type = c_int * len(data)
# 建立陣列
array = Array_Type(*data)
print(list(array))  # [3, 2, 1, 5, 4, 7, 6]
py_lib.sort_array(array, len(array))
print(list(array))  # [1, 2, 3, 4, 5, 6, 7]1.2.3.4.5.6.7.8.9.10.11.12.13.14.

排序實現完成,這裡的陣列是 Python 傳過去的,並且進行了原地修改。那 Rust 可不可以返回陣列給 Python 呢?從理論上來說可以,但實際不建議這麼做,因為你不知道返回的陣列的長度是多少?

如果你真的想返回陣列的話,那麼可以將陣列拼接成字串,然後返回。

複製

use std::ffi::{c_char, CString};

#[no_mangle]
pub extern "C" fn create_array() -> *mut c_char {
    // 篩選出 1 到 50 中,能被 3 整除的數
    // 並以逗號為分隔符,將這些整數拼接成字串
    let vec = (1..=50)
        .filter(|c| *c % 3 == 0)
        .map(|c| c.to_string())
        .collect::<Vec<String>>()
        .join(",");
    CString::new(vec).unwrap().into_raw()
}1.2.3.4.5.6.7.8.9.10.11.12.13.

編譯之後交給 Python 呼叫。

複製

from ctypes import *

py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")

# 只要是需要釋放的堆記憶體,都建議按照 c_void_p 來解析
py_lib.create_array.restype = c_void_p
# 此時拿到的就是指針保存的地址,在 Python 裡面就是一串整數
ptr = py_lib.create_array()
# 由於是字串首字元的地址,所以轉成 char *,拿到具體內容
print(cast(ptr, c_char_p).value.decode("utf-8"))
"""
3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48
"""
# 此時我們就將陣列拼接成字串返回了
# 但是堆區的 CString 還在,所以還要釋放掉,呼叫 free 函數即可
# 注意:ptr 只是一串整數,或者說它就是 Python 的一個 int 對象
# 換句話說 ptr 只是保存了地址值,但它不具備指針的含義
# 因此需要再使用 c_void_p 包裝一下(轉成指針),才能傳給 free 函數
py_lib.free(c_void_p(ptr))1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.

因此雖然不建議返回陣列,但將陣列轉成字串返回也不失為一個辦法,當然除了陣列,你還可以將更複雜的結構轉成字串返回。

傳遞結構體

結構體應該是 Rust 裡面最重要的結構之一了,它要如何和外部互動呢?

複製

use std::ffi::c_char;

#[repr(C)]
pub struct Girl {
    pub name: *mut c_char,
    pub age: u8,
}

#[no_mangle]
pub extern "C" fn create_struct(name: *mut c_char, age: u8) -> Girl {
    Girl { name, age }
}1.2.3.4.5.6.7.8.9.10.11.12.

因為結構體實例要返回給外部,所以它的欄位類型必須是相容的,不能定義 C 理解不了的類型。然後還要設定 #[repr(C)] 屬性,來保證結構體的記憶體佈局和 C 是相容的。

下面通過 cargo build 命令編譯成動態庫,Python 負責呼叫。

複製

from ctypes import *

py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")

class Girl(Structure):

    _fields_ = [
        ("name", c_char_p),
        ("age", c_uint8),
    ]

# 指定 create_struct 的返回值類型為 Girl
py_lib.create_struct.restype = Girl
girl = py_lib.create_struct(
    c_char_p("S 老師".encode("utf-8")),
    c_uint8(18)
)
print(girl.name.decode("utf-8"))  # S 老師
print(girl.age)  # 181.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.

呼叫成功,並且此時是沒有記憶體洩露的。

當通過 FFI 將資料從 Rust 傳遞到 Python 時,如果傳遞的是指針,那麼會涉及記憶體釋放的問題。但如果傳遞的是值,那麼它會複製一份給 Python,而原始的值(這裡是結構體實例)會被自動銷毀,所以無需擔心。

然後是結構體內部的欄位,雖然裡面的 name 欄位是 *mut c_char,但它的值是由 Python 傳過來的,而不是在 Rust 內部建立的,因此沒有問題。

但如果將 Rust 程式碼改一下:

複製

use std::ffi::{c_char, CString};

#[repr(C)]
pub struct Girl {
    pub name: *mut c_char,
    pub age: u8,
}

#[no_mangle]
pub extern "C" fn create_struct() -> Girl {
    let name = CString::new("S 老師").unwrap().into_raw();
    let age = 18;
    Girl { name, age }
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.

這時就尷尬了,此時的字串是 Rust 裡面建立的,轉成原始指針之後,Rust 將不再管理相應的堆記憶體(因為 into_raw 將所有權轉移走了),此時就需要手動堆記憶體了。

複製

from ctypes import *

py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")

class Girl(Structure):

    _fields_ = [
        ("name", c_char_p),
        ("age", c_uint8),
    ]

# 指定 create_struct 的返回值類型為 Girl
py_lib.create_struct.restype = Girl
girl = py_lib.create_struct()
print(girl.name.decode("utf-8"))  # S 老師
print(girl.age)  # 18
# 直接傳遞 girl 即可,會釋放 girl 裡面的欄位在堆區的記憶體
py_lib.free(girl)1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.

此時就不會出現記憶體洩露了,在 free 的時候,將變數 girl 傳進去,釋放掉內部欄位佔用的堆記憶體。

當然,Rust 也可以返回結構體指針,通過 Box 實現。

複製

#[no_mangle]
pub extern "C" fn create_struct() -> *mut Girl {
    let name = CString::new("S 老師").unwrap().into_raw();
    let age = 18;
    Box::into_raw(Box::new(Girl { name, age }))
}1.2.3.4.5.6.

注意:之前是 name 欄位在堆上,但結構體實例在棧上,現在 name 欄位和結構體實例都在堆上。

然後 Python 呼叫也很簡單,關鍵是釋放的問題。

複製

from ctypes import *

py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")

class Girl(Structure):

    _fields_ = [
        ("name", c_char_p),
        ("age", c_uint8),
    ]

# 此時返回值類型就變成了 c_void_p
# 當返回指針時,建議將返回值設定為 c_void_p
py_lib.create_struct.restype = c_void_p
# 拿到指針(一串整數)
ptr = py_lib.create_struct()
# 將指針轉成指定的類型,而類型顯然是 POINTER(Girl)
# 呼叫 POINTER(T) 的 contents 方法,拿到相應的結構體實例
girl = cast(ptr, POINTER(Girl)).contents
# 訪問具體內容
print(girl.name.decode("utf-8"))  # S 老師
print(girl.age)  # 18

# 釋放堆記憶體,這裡的釋放分為兩步,並且順序不能錯
# 先 free(girl),釋放掉內部欄位(name)佔用的堆記憶體
# 然後 free(c_void_p(ptr)),釋放掉結構體實例 girl 佔用的堆記憶體
py_lib.free(girl)
py_lib.free(c_void_p(ptr))1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.

不難理解,只是在釋放結構體實例的時候需要多留意,如果內部有欄位佔用堆記憶體,那麼需要先將這些欄位釋放掉。而釋放的方式是將結構體實例作為參數傳給 free 函數,然後再傳入 c_void_p 釋放結構體實例。

回呼函數

最後看一下 Python 如何傳遞函數給 Rust,因為 Python 和 Rust 之間使用的是 C ABI,所以函數必須遵循 C 的標準。

複製

// calc 接收三個參數,前兩個參數是 *const i32
// 最後一個參數是函數,它接收兩個 *const i32,返回一個 i32
#[no_mangle]
pub extern "C" fn calc(
    a: *const i32, b: *const i32,
    op: extern "C" fn(*const i32, *const i32) -> i32
) -> i32
{
    op(a, b)
}1.2.3.4.5.6.7.8.9.10.

然後看看 Python 如何傳遞迴調函數。

複製

from ctypes import *

py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")

# 基於 Python 函數建立 C 函數,通過 @CFUNCTYPE() 進行裝飾
# CFUNCTYPE 第一個參數是返回值類型,剩餘的參數是參數類型
@CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
def add(a, b):  # a、b 為 int *,通過 .contents.value 拿到具體的值
    return a.contents.value + b.contents.value

@CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
def sub(a, b):
    return a.contents.value - b.contents.value

@CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
def mul(a, b):
    return a.contents.value * b.contents.value

@CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
def div(a, b):
    return a.contents.value // b.contents.value

a = pointer(c_int(10))
b = pointer(c_int(2))
print(py_lib.calc(a, b, add))  # 12
print(py_lib.calc(a, b, sub))  # 8
print(py_lib.calc(a, b, mul))  # 20
print(py_lib.calc(a, b, div))  # 51.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.

成功實現了向 Rust 傳遞迴調函數,當然例子舉得有點刻意了,比如參數類型指定為 i32 即可,沒有必要使用指針。

小結

以上我們就介紹了 Python 如何呼叫 Rust 編譯的動態庫,再次強調一下,通過 ctypes 呼叫動態庫是最方便、最簡單的方式。它和 Python 的版本無關,也不涉及底層的 C 擴展,它只是將 Rust 編譯成 C ABI 相容的動態庫,然後交給 Python 進行呼叫。

因此這也側面要求,函數的參數和返回值的類型應該是 C 可以表示的類型,比如 Rust 函數不能返回一個 trait 對象。總之在呼叫動態庫的時候,庫函數內部的邏輯可以很複雜,但是參數和返回值最好要簡單。

如果你發現 Python 程式碼存在大量的 CPU 密集型計算,並且不怎麼涉及複雜的 Python 資料結構,那麼不妨將這些計算交給 Rust。

以上就是本文的內容,後續有空我們介紹如何用 Rust 的 PyO3 來為 Python 編寫擴展。PyO3 的定位類似於 Cython,用它來寫擴展非常的方便,後續有機會我們詳細聊一聊。


Python與Rust互動

Rust 可以與很多語言互動,前面我們已經介紹過與 C#JavaScript,本篇文章將介紹下 Rust 如何與 Python 互動。

0x01 PyO3

PyO3 是一個 Rust 的庫。通過它,使得我們從 Python 呼叫 Rust 變得非常容易。它可以用於建立本機 Python 擴展模組的工具。還支援從 Rust 二進制檔案運行 Python 程式碼並與之互動。

另外,如果要將 rust 編譯為 puthon 模組,還需要安裝 maturin。安裝方法如下:

複製程式碼pip install maturin   

PyO3 支援需要以下環境:

註:本文的所有操作默認你已經安裝 RustPython 環境。

  • Python 3.7 及更高版本(CPython 和 PyPy)

  • Rust 1.48 及更高版本

0x02 Python 呼叫 Rust 函數

toml 組態
[package]
name = "pyo3_polars_extension"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
name = "python_rust"
crate-type = ["cdylib"]


[features]
extension-module = ["pyo3/extension-module"]
default = ["extension-module"]

[dependencies]
pyo3 = "0.18.3"
編寫程式碼

為了簡單測試,我們使用 Rust 寫一個求和的程式碼。寫函數時跟我們平時寫沒啥區別,但是返回值需要返回 PyResult,並且還需要標註 #[pyfunction]#[pyfunction] 屬性用於從 Rust 函數定義 Python 函數。定義後,需要使用 wrap_pyfunction! 宏將該函數新增到模組中。

我們還需要建立一個與 toml 組態檔案中 lib.name 同名函數(當前也可以使用 #[pyo3(name = "custom_name")] 覆蓋模組名稱),並標註為 #[pymodule]#[pymodule] 過程宏負責將模組的初始化函數匯出到 Python。

完整的示例程式碼如下:

#![allow(unused)]
fn main() {
rust複製程式碼use pyo3::prelude::*;

/// 求兩個數的和
#[pyfunction]
fn sum(a: isize, b: isize) -> PyResult<isize> {
    Ok(a + b)
}

/// 一個用Rust實現的Python模組。
///
/// 這個函數的名字必須與`Cargo.toml`中的`lib.name`匹配
#[pymodule]
fn python_rust(_py: Python, module: &PyModule) -> PyResult<()> {
    module.add_function(wrap_pyfunction!(sum, module)?)?;
    Ok(())
}
}
編譯並安裝

官方推薦使用虛擬環境安裝模組,防止與其它模組衝突。我這裡還是喜歡使用 maturin build 先編譯,然後手動安裝。

  • 運行 maturin build 編譯
  • 使用 pip 安裝 target/wheels/模組名稱.whl
在 python 中使用函數

編寫測試程式碼:

python複製程式碼import python_rust

sum = python_rust.sum(5, 6)
print(sum)

成功輸出結果 11 。有沒有感覺到很簡單呢。

定義多個函數

當然了,我們還可以定義多個函數,我們只需要在模組函數中 add_function 就可以了。程式碼如下:

#![allow(unused)]
fn main() {
rust複製程式碼use pyo3::prelude::*;

/// 求兩個數的和
#[pyfunction]
fn sum(a: isize, b: isize) -> PyResult<isize> {
    Ok(a + b)
}

#[pyfunction]
fn multiple(a: isize, b: isize) -> PyResult<isize> {
    Ok(a * b)
}

/// 一個用Rust實現的Python模組。
///
/// 這個函數的名字必須與`Cargo.toml`中的`lib.name`匹配
#[pymodule]
fn python_rust(_py: Python, module: &PyModule) -> PyResult<()> {
    module.add_function(wrap_pyfunction!(sum, module)?)?;
    module.add_function(wrap_pyfunction!(multiple, module)?)?;
    Ok(())
}
}
.PHONY: build

build:
    maturin build --release

定義新函數後,再次使用模組,要記得先解除安裝再重新安裝!

0x03 小結

本篇文章簡單介紹了 Rust 使用 PyO3 來編寫 python 模組,其實 PyO3 的功能還有很多,接下來我們將繼續介紹如果在Rust中使用不同的類型。

給 C++ 使用者的 Rust 簡介

Rust 是最近受到廣泛注目的新語言。最早由 Mozilla 資助開發,後來因為 Dropbox 使用 Rust 改寫檔案系統服務[1]而聲明大噪。目前 Rust 是很活躍的開源專案,有超過一千名開發者共同開發,大約一至兩個月就會有一次 minor release。

設計程式語言最困難的地方在於選擇,沒有一個語言是上山下海無所不能的,而 Rust 也不例外。Rust 的目標是成為高效率、易於平行運算的系統程式語言,因此它選擇了以下的特性:

  • 靜態型別 (static-typed)
  • 區分 mutable 與 immutable,所有變數預設為 immutable,盡可能減少 mutable state
  • 使用 tagged union 與 pattern matching
  • 不使用動態垃圾回收 (garbage collection),而使用靜態的 RAII
  • 使用 Move semantics 避免複製物件
  • 使用 borrow checker 確保 memory safety 與 thread safety

因此,對於習慣主流程式語言的使用者來說,Rust 的學習曲線非常陡峭,光是要讓程式碼通過編譯就要花上不少時間。接下來這一系列的文章,是以 C++ 使用者為對象,介紹 Rust 的各種語言功能以及背後的設計目標,希望各位可以看得很愉快。

Hello World

先從每個語言都會有的 hello world 開始吧:

// hello.rs
fn main() {
    println!("hello world");
}

編譯與執行方法如下:

$ rustc -o hello hello.rs
$ ./hello
hello world

從這個最簡單的範例可以看出與 C++ 相同的地方:

  • 註解也是 //,你也可以用 /* */ 寫多行註解。
  • 程式也是以 main 為進入點。
  • 函式也同樣用大括號包起整個結構,每行敘述使用 ; 作為結尾。

不一樣的地方則是:

  • 沒有 #include
  • 需要用 fn 關鍵字來定義函式。
  • main 沒有回傳值。
  • println! 函式名稱多了一個驚嘆號。

rustc 會自動幫你引入一部份的標準函式庫 (std::prelude),因此你不需要為了印一行字額外引入函式庫。另外 println 後面的驚嘆號代表它其實不是函式,而是巨集 (macro)。由於本文重點不在巨集,因此我們只要先知道 println! 可以拿來當 printf 那樣用就可以了。

型別與變數

宣告變數的方法是使用 let 關鍵字:

fn main() {
    let x = 10;
    let y: f32 = 3.14;
    println!("x = {}, y = {}", x, y); // x = 10, y = 3.14
}

Rust 會自動推導型別,因此 x 的型別是 i32,意指 32bit signed integer。你也可以在變數名稱後加上冒號來指定型別,因此 y 的型別是 32bit floating point,而不是 floating point literal 預設的 f64

Rust 的內建型別及對應的 C++ 型別如下:

Rust typeC++ type說明
boolbool布林值
i8int8_t8-bit 有號整數,使用二補數表示負值
u8uint8_t8-bit 無號整數
i16int16_t16-bit 有號整數,使用二補數表示負值
u16uint16_t16-bit 無號整數
i32int32_t32-bit 有號整數,使用二補數表示負值
u32uint32_t32-bit 無號整數
i64int64_t64-bit 有號整數,使用二補數表示負值
u64uint64_t64-bit 無號整數
usizesize_t可表達記憶體空間內最大物件大小的無號整數型別,常用來表示 array index
isizeptrdiff_t上述型別的有號版本,可用來表達兩個 array index 的差異
f32float [2]IEEE754 規範的 32-bit 浮點數
f64double [2]IEEE754 規範的 64-bit 浮點數
charchar32_t [3]使用 UTF-32 表達的 Unicode 字元

Mutable & Immutable

雖然講起來很矛盾,但預設情況下變數是不可變的 (immutable):

#![allow(unused)]
fn main() {
let x = 10;
x = x + 1; // error: re-assignment of immutable variable `x`
}

若要讓變數可以重新賦值,需要使用 mut 關鍵字來宣告:

#![allow(unused)]
fn main() {
let mut y = 10;
y = y + 1; // ok
}

有很多理由支持讓 immutable 成為預設,比如說 compiler 的最佳化或是減少 race condition。在後續的文章中,我會更進一步討論。

Struct & Tuple

Rust 的 struct 宣告方式與 C++ 大同小異,差別僅在於各成員型別的位置、使用逗號作為分隔、以及結尾不需要加分號:

struct Foo {
    x: i32,
    y: f64,
}

fn main() {
    let foo = Foo { x: 10, y: 2.5 };
    println!("foo.x = {}, foo.y = {}", foo.x, foo.y);
}

而 tuple 其實是個語法糖 (syntactic sugar),相當於使用編號當作成員名稱的 struct:

#![allow(unused)]
fn main() {
let triple = (10, 3.14, 'x');
println!("triple = ({}, {}, {})", triple.0, triple.1, triple.2)
}

編譯器會把它轉變成這樣 (以下為示意,實際上宣告 struct 時不能拿數字當成員名稱):

#![allow(unused)]
fn main() {
struct Triple {
    0: i32,
    1: f64,
    2: char,
}
let triple = Triple { 0: 10, 1: 3.14, 2: 'x'};
println!("triple = ({}, {}, {})", triple.0, triple.1, triple.2)
}

值得注意的是,沒有任何元素的 tuple,也就是 0-tuple,也是一個合法的型別,稱之為 unit type。它具有唯一一個可能的值,就是空的 tuple,稱之為 unit

#![allow(unused)]
fn main() {
let unit: () = (); // 完全合法,雖然你沒辦法拿這個變數做什麼事
}

Move Semantics

若是沒有覆載賦值運算子,C++ 的 struct 具備 value-type semantics,意即使用等號賦值或進行參數傳遞時,會複製整個物件的內容。而 Java class 則具備 reference-type semantics,使用等號僅僅複製物件的位址,它們仍然會影響同一個物件的內容。

Rust 並沒有 class,那麼 rust 的 struct 是 value-type 還是 reference-type 呢?我們試著用最簡單的做法來判定 value-type 與 reference-type:宣告一個物件,用等號賦值給另一個物件並修改其內容,然後檢查原物件的值是否變動。對 value-type 來說是不變動的,而對 reference-type 來說,因為兩個變數實際指向同一塊記憶體,因此內容會變動。然而,這兩種狀況都不會發生在 Rust 上面,因為 compiler 把它擋下來了。

struct Point {
	x: i32,
	y: i32,
}

fn main() {
	let mut foo = Point { x: 10, y: 20 };
	let mut bar = foo;
	bar.x = 30;
	println!("foo.x = {}", foo.x); // error: use of moved value `foo.x`
}

Rust struct 具備了 move semantics,使用等號賦值時,資料並不是「複製」,而是「移動」到左值上。右值在移動後,就會成為未初始化的物件,因此 Rust 禁止你對它進行操作。如果你還是有點難以想像,把它理解成 C++11 的 std::move 就可以了:

#include <utility>
#include <iostream>

struct Point {
    int x, y;
};

int main() {
    auto foo = Point { x: 10, y: 20 };
    auto bar = std::move(foo);
    bar.x = 30;
    std::cout << "foo.x = " << foo.x << std::endl;
    return 0;
}

這段程式碼可以通過編譯,然而如果你把 Point 換成其它實作 move constructor 的物件 (比如 std::string),那麼在 std::move(foo) 之後,很可能 foo 會成為內容未初始化的物件,印出其內容會造成 undefined behavior。

為了使用上的方便,Rust 的基本型別,也就是上面那張表格中的所有型別,都具備可複製的特性。因此使用等號賦值時,進行的動作是「複製」,讓你可以繼續操作右值。在後續的文章中,我會更進一步講解 Rust 的 move semantics。

表達式

Rust 是 expression-oriented language,大部份的流程控制結構,比如說 if,其實都是可以求值的表達式。

#![allow(unused)]
fn main() {
let x = -10;
let abs_x = if x >= 0 {
                x
            } else {
                -x
            };
}

這看起來其實就是 C++ 的 ?: 運算子。然而,Rust 可以在表達式中用分號進行區隔,並使用最後一個表達式當作結果,因此可以組合出複雜的表達式:

#![allow(unused)]
fn main() {
let year = 2016;
let is_leap = {
                  let div_4 = (year % 4 == 0);
                  let div_100 = (year % 100 == 0);
                  let div_400 = (year % 400 == 0);
                  if div_400 || (div_4 && !div_100) {
                      "is"
                  } else {
                      "is not"
                  }
              };
println!("Year {} {} a leap year.", year, is_leap);
}

分號可以用來分隔表達式,最後一個不帶分號的表達式會成為整個表達式的結果,因此 is_leap 會根據條件判斷,成為 "is""is not"。注意第七行與第九行都不能加分號,要是最後一個運算式也加上分號,那麼整個運算式的結果會變成 (),也就是那個沒啥用的 0-tuple。而在第 11 行的分號則用來區隔 let 變數宣告與 println!,是一定要加上去的。

函式

前面提到 Rust 使用 fn 來宣告函式,而且回傳型別寫在後面,看起來很像 C++11 裡面新的函式宣告法:

#![allow(unused)]
fn main() {
fn square(x: f64) -> f64 {
    return x * x;
}
}

函式本體也是可以使用分號區隔的表達式,最後一個不帶分號的表達式會自動成為函式的回傳值,因此上一段檢查閏年的函式可以這樣寫:

#![allow(unused)]
fn main() {
fn is_leap(year: i32) -> bool {
    let div_4 = (year % 4 == 0);
    let div_100 = (year % 100 == 0);
    let div_400 = (year % 400 == 0);

    div_400 || (div_4 && !div_100)
}
}

你可以用 tuple 輕易讓函式回傳多個值:

#![allow(unused)]
fn main() {
// 對兩個數字做排序
fn reorder(x: i32, y: i32) -> (i32, i32) {
    if x > y {
        (y, x)
    } else {
        (x, y)
    }
}
}

即使函式不回傳任何值,它還是有回傳型別,也就是上面提到那個好像沒啥用的 0-tuple。

fn say_hello() -> () { // -> () 可省略
    println!("hello world");
}

fn main() { // 若無回傳型別,rust 會自動加上 -> ()
    let result = say_hello(); // 合法,result 的值為 ()
}

這看起來好像沒什麼用,畢竟 0-tuple 什麼事都做不了。然而,當你要寫泛型函式 (generic function) 時,你會跪在電腦前感謝這個設計。

泛型

如同 C++ 那般,Rust 也可以利用模版 (template) 來達成泛型程式設計,語法也非常接近 C++:

struct Point<T> { // 相當於 template<typename T> struct Point
    x: T,
    y: T,
    z: T,
}

fn main() {
    let point_i32 = Point { x: 10, y: 20, z: 30 };
    let point_f64 = Point { x: 2.078, y: 0.454, z: 3.1415 };
}

與 C++ 不同的是,大部份情況下 Rust 都能藉由前後文來自動推導出正確的模版型別,因此上面的例子中並不需要特別加入 <i32> 或是 <f64>,直接用 Point 即可。

泛型非常適合用來實作容器型別,比如 Rust 提供的 Vec 泛型容器,就相當於 C++ 的 std::vector

fn main() {
    let mut array = Vec::new();
    array.push(1);
    array.push(2);

    println!("{}", array[0] + array[1]);
}

同樣地,因為 Rust 從第三行的 push(1) 判斷出 array 的元素型別為 i32,因此在第二行就不需要寫明 Vec<i32>::new(),直接寫 Vec::new() 即可。

除了泛型類別,模版也可以用來定義泛型函式,然而與目前 C++ 不同的地方是,在 Rust 中,對泛型型別進行操作前,必需為它標上 constraint:

#![allow(unused)]
fn main() {
fn sum<T: Add>(a: T, b: T) -> T::Output {
    a + b
}
}

這邊 Add 意指 T 必需是可以使用加號相加的型別,包括整數及浮點數都包括在內。由於相加後輸出型別不一定仍然為 T,因此這個函式的回傳型別是 T::Output。Rust 也支援運算子覆載 (operator overloading),只要你的自訂型別定義了加號操作以及輸出型別,那麼這個自訂型別也可以直接傳入 sum 進行運算。

總結

這篇文章中,我把重點放在 Rust 最核心的語言功能上,甚至省略了陣列與字串處理,因為講解這部份就無可避免會提到 borrow checker。在後續幾篇文章中,我將會繼續深入解釋 move semantics 與 borrow checker。

原文鏈接: https://www.youtube.com/watch?v=rDoqT-a6UFg

翻譯:trdthg

選題:trdthg

本文由 Rustt 翻譯,RustCn 榮譽推出

可視化 Rust 各數據類型的內存佈局

本文已獲得作者翻譯許可。由於譯者個人能力有限,如有翻譯錯誤,希望讀者加以指正。
視頻版翻譯:B站視頻鏈接

// file: main.rs
fn main() {
    println!("Hello World!");
}

當我們使用 Rust 中編寫程序時,由於 Rust 的 生命週期和所有權模型,你最好為程序可能用到的數據結構做一些前期設計,不然 Rust 編譯器可能讓你十分痛苦。瞭解每個數據類型的內存佈局有助於鍛鍊你的直覺,可以提前規避一些編譯錯誤和性能問題。

在這個文章裡,我們會討論

  • 在計算機運行二進制文件時發生了什麼?
  • 常見數據類型的內存佈局 (包括:整形,元組,切片,向量,字符串,結構體,枚舉,智能指針,特徵對象,還有各種 Fn 特徵)

二進制數據段

當你編寫一個 Rust 程序時,要麼直接調用 rustc,要不就是通過 cargo 去生成一個可執行文件。

$ rustc main.rs
$ cargo build

這個二進制文件以一種特定的格式存儲數據。對於 linux 系統,最常見的格式是 elf64 。不同的操作系統比如 linux, mac, windows 使用不同的格式。雖然二進制文件的格式不盡相同,但是它在各種的操作系統中的運行方式幾乎相同。

常見的二進制文件一般由 文件頭 + 分區 組成。 對於 elf 格式的二進制文件,它的結構大致如下圖所示:

段的數量根據編譯器而不同。這裡只展示了一些重要的一些段。

當你運行二進制文件時

elf64 格式的二進制文件為例,在程序運行時,內核會為程序分配一段連續的內存地址,並將這些分區映射到內存中去。

注意:這裡的內存地址並不是內存條裡實際的內存地址。但是當程序開始使用內存時,內核和硬件會把它們映射到真正的物理內存地址。這被稱為 虛擬地址空間。一個正在運行的程序被稱為一個進程。從進程的角度來看,它只能看到一段連續的內存,從 0 到地址高位的最大值。

下面我們會介紹進程地址空間中各個區域的作用:

  1. 代碼段 (text)

    代碼段包含了可執行指令的集合。

    編譯器能把我們用高級語言寫的程序轉換為 CPU 可以執行的機器指令,代碼段就包含了這些指令。這些指令根據 CPU 架構而有所不同。編譯給 x86-64 架構 CPU 運行的二進制文件不能在 ARM 架構的 CPU 上運行。

    代碼段是 只讀 的,運行的程序不能更改它。

  2. 數據段 (data)

    數據段包含 已經初始化 過的數據。比如全局變量,全局靜態變量,局部靜態變量。

  3. BSS 段 (bss)

    bss 代表 Block started by symbol, 這裡保存著 未被初始化 過的全局變量。由於 bss 段的變量未被初始化,這一段並不會直接佔據二進制文件的體積,它只負責記錄數據所需空間的大小

  4. 地址高位

    內核會把一些額外的數據,比如環境變量,傳遞給程序的參數和參數的數量映射到地址高位。

堆 & 棧

堆棧簡介

當程序運行時(運行態),還需要需要另外兩個域:堆和棧

  • 操作系統使用棧存儲一個進程的抽象細節,包括 (進程名字,進程 ID 等)。

  • 一個進程至少有一個執行線程,每一個線程都有自己的棧內存。

  • 在 64 位的 linux 系統上,Rust 程序為主線程分配 8MB 的棧內存。對於用戶創建的其他線程,rust 標準庫支持自定義大小,默認的大小是 2MB。

  • 棧內存的空間會從地址高位向低位增長,但是不會超過線程可以擁有的最大值。對於主線程來說就是 8MB。如果它使用的棧內存超過了 8MB,程序就會被內核終止,並返回一個 stackoverflow 錯誤。

  • 棧內存被用於執行函數 (見下方對棧的具體講解)。

雖然主線程的棧內存大小有 8MB,但是這 8MB 也不會被立即分配,只有當程序開始使用時,內核才會開始為它分配內存。

  • 所有線程共享一塊堆內存
  • 堆內存從地址低位向高位增長。

操作系統通常會提供一些接口讓我們檢查程序運行時的內存映射狀態,對於 linux 系統,你可以在 /proc/PID/maps 文件中查看

下面展示了一個進程的映射狀態(部分):

$ cat /proc/844154/maps
55e6c3f44000-55e6c412c000 r-xp 00000000 103:03 22331679                  /usr/bin/fish
55e6c412c000-55e6c4133000 r--p 001e7000 103:03 22331679                  /usr/bin/fish
55e6c4133000-55e6c4134000 rw-p 001ee000 103:03 22331679                  /usr/bin/fish
55e6c4134000-55e6c4135000 rw-p 00000000 00:00 0
55e6c4faa000-55e6c5103000 rw-p 00000000 00:00 0                          [heap]
7fd62326d000-7fd62326f000 r--p 00034000 103:03 22285665                  /usr/lib/ld-linux-x86-64.so.2
7fd62326f000-7fd623271000 rw-p 00036000 103:03 22285665                  /usr/lib/ld-linux-x86-64.so.2
7ffecf8c5000-7ffecf8f5000 rw-p 00000000 00:00 0                          [stack]

你可能會想問:堆內存和棧內存是否會相互覆蓋?因為他們兩個向對方的方向增長。

通過用 stack 的低位減去 heap 的高位

>>> (0x7ffecf8c5000 - 0x55e6c5103000) / (10 ** 12)
46.282743488512

差距為 47TB,所以棧堆衝突的情況幾乎不可能出現

如果確實發生了,內核會提供守衛去終止程序。注意,這裡的內存是指虛擬內存,並非電腦的真實內存大小。

CPU 字長

虛擬內存地址的範圍由 CPU 字長 (word size) 決定,字長是指 CPU 一次可以並行處理的二進制位數,對於 64 位的 CPU 來說,它的字長為 64 位 (8 字節)。CPU 中大多數或者全部寄存器一般都是一樣大。

因此可以得出:64 位 CPU 的尋址空間為 0 ~ 2^64-1。而對於 32 位的 CPU 來說,它的尋址空間只有從 0 到 2^32,大概 4GB。

目前,在 64 位 CPU 上,我們一般只使用前 48 位用於尋址,大小大概是 282TB 的內存

>>> 2**48 / (10**12)
281.474976710656

這其中,只有前 47 位是分配給用戶空間使用,這意味著大概有 141TB 的虛擬內存空間是為我們的程序分配的,剩下的位於地址高位的 141TB 是為保留給內核使用的。如果你去查看程序的虛擬內存映射,你能使用的最大內存地址應該是 0x7fffffffffff

>>> hex(2**47-1)
'0x7fffffffffff'

棧內存

接下來讓我們深入瞭解棧內存的用途

在這個例子中,整個程序只有一個主線程在運行,我們在 main 裡調用了 add1 函數。

fn main() {
    let a = 22;
    let b = add_one(a);
}

fn add_one(i: i32) -> i32 {
    i + 1
}

棧主要用來保存正在調用的函數的數據 (包括函數參數,函數的局部變量,和它的返回地址)。為一個運行中的函數分配的總內存被稱為一個 棧幀

  1. main 函數是程序的入口,首先 main 函數的棧幀被創建。

    main 函數內部有一個兩個 i32 類型的局部變量 ab,大小都是 4 個字節,其中 a 的值為 22。main 函數的棧幀會確保有足夠的空間去保存這些局部變量。

    ESP 和 EBP 寄存器內分別保存著棧頂指針和棧底指針,用來追蹤當前的棧的頂部和底部。

    圖片名稱
  2. main 函數調用 add1 時,一個新的棧幀被創建用來保存 add1 函數的數據。棧頂指針被修改為新棧的頂部。

    圖片名稱
    1. add1 函數要接受一個 i32 類型的參數,因此 4 字節的空間會被保留在 add1 函數的棧幀上。
    2. add1 函數並沒有局部變量
    3. 棧幀還會保存一個返回地址,當函數運行結束後,會根據該返回地址回到之前的指令。
  3. 函數調用結束

    當函數調用結束後,就會把返回值 23 賦值給局部變量 b。同時棧頂指針也被修改。

    圖片名稱

    注意:函數運行結束後,add1 的棧幀並沒有被釋放。當你的程序開始調用下一個函數時,新的棧幀會直接將其覆蓋。對於棧來說,開闢和釋放內存只需要修改棧指針即可。

由此可見,因為在棧上開闢和釋放內存只需要移動指針,不需要進行任何系統調用,它的效率是很高的。

當然棧也有一些限制:

  • 只有在編譯時已知大小的變量才能被存儲在棧上。
  • 函數不能返回一個位於函數內部的局部變量的引用

如果你把 add_one 改成下面的樣子,就會編譯失敗:

fn add_one(i: i32) -> &'static i32 {
    let result = i + 1;
    &result
}
error[E0515]: cannot return reference to local variable `result`
 --> src/main.rs:8:5
  |
8 |     &result
  |     ^^^^^^^ returns a reference to data owned by the current function

根據我們之前介紹過棧的工作原理,假設你現在返回了一個函數內局部變量的引用,但是當函數返回時,本質上函數的內存就被釋放了。當下一個函數被調用時,它的棧幀就會重寫這塊內存空間。

在一個帶有 GC 的語言裡,編譯器能夠檢測到這種覆蓋,並在會為這個變量在堆上分配一塊空間,並返回它的引用。但是在堆上分配會帶來部分額外開銷。因為 Rust 沒有 GC,而且不會強制你去顯式的分配堆內存,所以這裡會編譯失敗。

堆內存

在這個例子裡,我們在 main 函數中調用了 heap 函數。

fn main() {
    let result = heap();
}

fn heap() -> Box<i32> {
    let b = Box::new(23);
    b
}

首先會為兩個函數再棧上創建棧幀。接著使用 box 將 23 分配在堆上。然後把 23 在堆上的地址賦值給了變量 bbox 只是一個指針,所以棧上有足夠的空間去保存 box

圖片名稱

在 64 位系統上,指針的大小是 8 字節,所以在棧上的變量 b 的大小是 8 字節。而 b 指向的變量 23 是 i32 類型,它在堆上只需要佔用 4 字節。

當函數調用結束後,heap 函數返回的 box 指針就會被保存在 main 函數的局部變量裡。

圖片名稱

當你對棧上的數據進行賦值操作時,它的棧內存就會被直接 copy 過去。在這個例子裡,用來保存 box 的 8 個字節就是從 heap 函數的棧幀直接複製到 main 的局部變量 result。現在即使 heap 函數的棧幀被釋放,result 變量依然保存著數據的地址。堆允許你共享變量。

內存分配器

我們之前提到過每個線程都有各自的棧內存,他們共享一塊堆內存。

假設你的程序不斷在堆上分配新的數據,現在堆內存幾乎耗盡了,需要對堆內存進行擴容。

圖片名稱

程序的內存分配器一般會使用系統調用請求操作系統分配更多內存。對於 linux 系統來說,一般是 brk 或者 sbrk 系統調用。

在 Rust 裡,堆內存分配器需要實現 GlobalAlloc 特徵。你幾乎不會直接用到它,編譯器會在需要時插入合適的系統調用。

// /rust/library/std/src/sys/unix/alloc.rs
#[stable(feature = "alloc_system_type", since = "1.28.0")]
unsafe impl GlobalAlloc for System {
    #[inline]
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        if layout.align() <= MIN_ALIGN && layout.align() <= layout.size() {
            libc::malloc(layout.size()) as *mut u8
        }
        ...
    }
    ...
}

你可能很熟悉 C 語言裡的 malloc 函數,但是它並不是系統調用,malloc 依然會調用 brk 或者 sbrk 去請求內核。Rust 的內存分配器依靠 C 標準庫裡提供的 malloc 函數,如果你使用像 ldd 這樣的工具去檢查二進制文件依賴的動態鏈接庫,你應該會看到 libc

$ ldd target/debug/demo
    linux-vdso.so.1 (0x00007fff60bd8000)
    libc.so.6 => /usr/lib/libc.so.6 (0x00007f08d0c21000)
    /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f08d0ebf000)

Linux 下 Rust 默認使用 GNU 作為鏈接器,因此 Rust 二進制文件依賴於操作系統上的 C 標準庫或者 libc 庫。libc 更像是操作系統的一部分,使用像 libc 這樣的動態鏈接庫有助於減少二進制文件體積。

同時,內存分配器也不總是依賴於系統調用在堆上分配內存:

  1. 每次程序使用 box 等把數據分配在堆上時,程序的內存分配器都會成塊的請求內存去減少系統調用的次數。

  2. 堆和棧不一樣,內存不一定總是在堆的末尾被釋放。當一些地方的內存被釋放後,它並沒有立即返還給操作系統,內存分配器會追蹤內存分頁,知道那些頁正在使用,那些頁被釋放了。所以當需要更多堆內存時,它可以直接使用這些已經釋放但還未歸還的內存分頁。

現在你應該知道為什麼分配堆內存比棧內存更消耗性能了。分配堆內存可能使用到系統調用,而且內存分配器每一次分配內前,都必須從堆上找到一個空閒內存塊。

Rust 各數據類型的內存佈局

整形

長度 (byte)長度 (bit)有符號無符號
1 字節8 位i8u8
2 字節16 位i16u16
4 字節32 位i32u32
8 字節64 位i64u64
16 字節128 位i128u128

有符號和無符號整形的名字已經展示了它所佔的位數,比如 i16u16 在內存都是 16 位 (2 字節)。它們都被完整的分配在函數的棧幀上。

isizeusize 的大小則取決於你的系統,32 位系統就佔用 4 字節,64 位系統就佔用 8 字節。

字符型

char Rust 的字符不僅僅是 ASCII,所有的 Unicode 值都可以作為 Rust 字符。 例如 a\u{CA0}*\n🦀

char 類型長度是 4 字節,直接分配在棧上

元組

元組是一些類型的集合

let a: (char, u8, i32) = ('a', 7, 354);

比如這裡,變量 a 包含了 char, u8, i32 三種數據類型,它的內存佈局就是將各個成員依次排列。

在這裡 char 佔用 4 字節,u8 佔用 1 字節,i32 佔用 4 字節。因為這三種類型都是隻在棧上分配的,所以整個元組也全在棧上分配。

雖然看起來這個元組只會佔用 9 字節的空間,但是其實並不是這樣,你可以用 size_of 去查看這個元組佔用的真正字節數

std::mem::size_of::<T>()

size_of 和 align_of

use std::mem::{size_of, align_of};

size_of::<(char, u8, i32)>(); // 12 字節

align_of::<(char, u8, i32)>(); // 4 字節

所有的數據類型還有一個對齊屬性,你可以通過 align_of 查看。

數據類型的大小必須是對齊屬性的整數倍。這一點不僅僅是 Rust,所有的編譯器都是這樣。數據對齊對 CPU 操作及緩存都有較大的好處,有助於 CPU 更快的讀取數據。

對於這個元組,它的對齊屬性值是 4,因此它佔用的字節數是 12。剩下的 3 字節會被編譯器填充空白數據

引用

接下來是引用類型 &T

let a: i32 = 25;

let b: &i32 = &a;

a 是 i32 類型,b 是對 a 的引用。

接下來,我不會在詳細展示每個數據的字節大小,我們將重點去關注整體,關注他們是存儲在堆上還是棧上。

在這裡,a 存儲在棧上,它佔據 4 個字節。b 也存儲在棧上,裡面保存了變量 a 的地址。引用類型的大小取決於你的機器位數,所以 64 位系統上它佔 8 字節。

如果我們再用 c 保存 b 的引用,c 的類型就是 &&i32

let c: &&i32 = &b;

引用也能指向堆上的數據。

可變引用也有相同的內存佈局。

可變引用和不可變引用的區別是他們的使用方式,以及編譯器為可變引用添加的額外限制。

數組

let a: [i32; 3] = [55, 66, 77];

一個數組的大小是固定的,而且它的大小是數據類型的一部分。數組中的每個元素都會在棧上相鄰排放。但是當數組創建後,它的大小就不能再改變。

注意:只有大小固定而且在編譯時已知的數據類型才能存儲在棧上。

Vec

Vec 類型是可擴容的,它的大小能夠改變,你可以用它代替數組。

let v: Vec<i32> = vec![55, 66, 77];

這裡我們的變量 v 存儲了和數組相同的數據,但是它是在堆上分配的。

變量 v 在棧上佔用的大小是固定的,包含 3 個 usize

  • 第一個表示數據在堆上的地址,
  • 剩下的兩個表示 Vec 的容量和長度。

容量表示 Vec 的最大空間。當我們向 Vec 中添加更多數據時,如果元素個數還沒有達到容量大小,Rust 就不必為堆內存分配更多空間。

如果長度和容量已經相等了,我們還要向 Vec 添加更多數據,Rust 就會在堆中重新分配出一塊更大的內存,將原數據複製到新的內存區域,並更新棧中的指針。

切片

let s1: [i32] = a[0..2];

let s2: [i32] = v[0..2];

切片 [T] 和數組非常相似,但是不用指定大小。切片就像是底層數組的一個視圖,s1 表示數組 a 的前兩個元素,s2 表示向量的前兩個元素。

由於切片沒有指定元素數量,編譯時 Rust 編譯器不知道它具體佔了多少字節。同時,你也不能將切片存在變量中,因為它沒有已知大小,所以不能被分配在棧上,這樣的類型被稱為 DST 動態大小類型

還有其他的 DST 類型,比如字符串切片和特徵對象。

如果你嘗試運行上面的代碼,應該會編譯失敗:

error[E0277]: the size for values of type `[i32]` cannot be known at compilation time
 --> examples/vec.rs:8:9
  |
8 |     let s1: [i32] = a[0..2];
  |         ^^ doesn't have a size known at compile-time
  |
help: consider borrowing here
  |
8 |     let s1: [i32] = &a[0..2];
  |                     +

因此,幾乎在任何情況下,我們只會使用到切片的引用 &[T]。被引用的數據既能在棧上,也能在堆上:

我們之前說過,引用只是一個指針,它佔據一個 usize 去存儲它所指向的數據的地址。

但是當你用指針去指向一個動態大小類型時 (比如切片),Rust 會使用一個額外的 usize 去存儲數據的長度。這種引用也叫做 胖指針 (將一些附加信息和指針一起存儲)。

切片引用可以用兩個 usize 表示,所以它可以存在棧上。

字符串

與字符串相關的有三種類型:String, str, &str,他們分別對應 Vec, [T]&[T}

字符串類型 String 的內存佈局和向量相同,唯一的區別是,字符串類型必須是 UTF-8 編碼。

以下面的代碼為例:

let s1: String = String::from("hello");

但是,如果你把一個字符串直接保存在變量中:

let s2: &str = "hello";

s2 的類型就會變成字符串切片的引用,這個字符串的數據不會在堆上,而是直接存儲在編譯好的二進制文件中。這種字符串有 'static 的生命週期,它永遠不會被釋放,在程序運行時都是可用的。

據我所知,Rust 不會指定字符串被保存在文件的那個部分,但是很可能就在代碼段 (text segment)

和切片引用一樣,對字符串的切片的引用也是一個胖指針,包含兩個 usize,一個用來存儲字符串的內存起始地址,另一個存儲字符串長度。

你不能直接使用字符串切片 str:

// error: size can not be known at compile time
let s: str = s1[1..3];

對字符串的切片引用是可行的:

let s: &str = &s1[1..3];

結構體

Rust 有三種結構體類型:結構體,元組結構體 (Tuple Struct) 和單元結構體 (Unit-like Struct)。

普通結構體:

struct Data {
    nums: Vec<usize>,
    dimension: (usize, usize),
}

元組結構體:

struct Data(Vec<usize>);

單元結構體:

struct Data;

單元結構體不保存任何數據,所以 Rust 編譯器甚至不會為他分配內存。

另外兩種結構體的內存排布非常類似於之前所說的元組,我們以普通的結構體為例:

struct Data {
    nums: Vec<usize>,
    dimension: (usize, usize),
}

它有兩個字段,一個 Vec 和一個元組,結構體的各個成員會在棧上依次相鄰排列。

  • Vec 需要佔用 3 個 usize,nums 的成員會被分配在堆上。
  • 元組需要佔用 2 個 usize

注意:我們在這裡忽視了內存對齊和編譯器填充的 padding。

枚舉

像結構體一樣,Rust 支持用不同的語法表示枚舉。

下面展示的是一個 C 風格的枚舉,在內存中他們被保存為從零開始的整數,Rust 編譯器會自動選擇最短的整數類型。

enum HTTPStatus {
    Ok,
    NotFound,
}

在這裡最大值為 1,因此該枚舉可以使用 1 字節存儲。

你也可以手動為枚舉的每個變體指定它的值:

enum HTTPStatus {
    Ok = 200,
    NotFound = 404,
}

這個例子裡最大的數是 404,需要至少 2 字節存儲。所以這個枚舉的每種變體都需要 2 字節。

枚舉值也可以選擇具體的類型

enum Data {
    Empty,
    Number(i32),
    Array(Vec<i32>)
}

在這個例子中

  • Empty 變體不存儲任何數據
  • Number 內部有一個 i32
  • Array 裡面有個 Vec

它們的內存佈局如下圖所示:

首先我們看 Array 變體:

首先是一個整數標記 2 佔用 1 字節,接著就是 Vec 所需的三個 usize ,編譯器還會填充一些空白區域讓他們內存對齊,所以這個變體需要 32 字節 (1 + 7 + 3 * 8)。

接著是 Number 變體,首先是整數標記 1,接著是 Number 裡存儲的 i32,佔用 4 字節。因為所有變體的大小應該是一致的,所以編譯器會為它們兩個都添加 Padding 達到 32 字節

對於 Empty,它只需要一個字節去存儲整數標記,但是編譯器也必須添加 31 字節的 Padding

所以,枚舉佔用的空間取決於最大變體佔用的空間。

減少內存使用的一個技巧就是降低枚舉最大變體佔用的內存:

enum Data {
    Empty,
    Number(i32),
    Array(Box<Vec<i32>>) // 使用 Box 代替
}

在這個例子裡,我們存除了 Vec 的指針,此時 Array 變體需要的內存只有 16 字節:

Box

Box 是一個指針指向堆上的數據,所以 Box 在棧上只需要 1 個 usize 去存儲地址。

在上個例子中,Box 指向了一個在堆上分配的 Vec。

如果向量裡面有值,這些值也會被存儲在堆上。指向數據的指針將保存在 Vec 的指針字段裡

對 Option 的優化

pub enum Option<T> {
    None,
    Some(T)
}

由於 Rust 不允許出現空指針,想要實現同樣的效果,你需要使用

Option<Box<i32>>

這能夠讓 Rust 編譯器確保不會出現空指針異常。

在其他語言裡,使用一個指針就能表示這兩種狀態。但是 Rust 卻需要一個額外的整數標記和隨之帶來的 padding,這會造成內存浪費。

編譯器能對此做出一些優化,如果 Option 裡是 Box 或者是類似的指針類型,編譯器就會省略掉整數標記,並使用值為 0 的指針表示 None。

這種特性使得 Rust 中被包裝在 Option 內的智能指針像其他語言裡的指針一樣,不會佔用多餘的內存。同時還能夠提前找到並消除空指針異常

Copy 和 Move

在繼續向下討論之前,讓我們先了解一下 Copy 和 Move

let num:i32 = 42;
let num_copy = num;

對於原始類型數據,他們的大小是在編譯時已知的,會被存儲在棧上。如果你將一個變量賦值給另一個變量,它得到的實際上是原始數據的一份拷,Rust 會逐位進行復制。

這兩個變量之後能同時使用

對於在堆上存儲的數據來說:

let v: Vec<String> = vec![
    "Odin".to_String(),
    "Thor".to_String(),
    "Loki".to_String(),
]

在這個例子裡,我們有一個在堆上分配的字符串向量。

變量 v 被保存在棧上,它需要 3 個 usize 去存儲 Vec 的信息,並指向數據在堆中的地址。

每個字符串也需要 3 個 usize 來存儲實際字符串的信息。

真正的字符串會被分配到堆上的其他地方。

從所有權角度來說,變量 v 擁有所有在堆上分配的內存。因為 Rust 沒有 GC,當變量 v 自己超出作用域後,它需要自己釋放自己擁有的堆內存。

接下來我們將 v 賦值給了 v2:

let v2 = v;

對於有 GC 的語言來說,程序會對變量 v 在棧上的數據進行了按位複製,最後 v2 也將擁有指向堆上數據的指針。

這種方案很節省內存,無論在堆中的數據有多大,我們只需要複製棧上的數據。垃圾回收器會追蹤堆內存的引用數量,當引用計數歸零,垃圾回收器會幫我們釋放堆內存。

但是 Rust 沒有 GC,它只有所有權模型。我們不清楚到底哪個變量需要對釋放內存負責。

另一種方案是:在賦值時為堆內存也創建一個副本。但是這會導致內存使用量升高,降低性能。

Rust 的選擇是讓用戶必須做出選擇:如果你在對變量賦值時想讓它擁有一份屬於自己的堆內存,你應該使用 clone 方法。如果你不使用 clone 方法,Rust 編譯器就不允許你再使用之前的變量。

我們把它稱為:變量 v 已經被 move 了,現在 v2 是數據的擁有者。當 v2 超出作用域時,它會負責釋放堆上的數據。

Rc

有時候我們想讓一個值擁有多個擁有者,大多數情況下,你可以用普通的引用去解決。但是這種方法的問題在於,當數據的擁有者超出作用域後,所有的引用也不能再繼續使用。

我們想要的是所有變量都是數據的擁有者,只有所有變量都超出作用域後,數據才會被釋放。Rc 智能指針通過引用計數能夠實現這個功能:

use std::rc::Rc;

let v: Rc<Vec<String>> = Rc::new(vec![
    "Odin".to_String(),
    "Thor".to_String(),
    "Loki".to_String(),
]);

let v2 = v.clone();
println!("{}, {}", v.capacity(), v2.capacity())

當你使用 Rc 去包裹一個 Vec 時,Vec 的 3 個 usize 會和引用計數一起分配在堆上。變量 v 在棧只佔用一個 usize,裡面存儲了 Rc 在堆上的地址。

現在你能通過克隆 v 來創建 v2,這個克隆不會克隆任何位於堆上的數據,他只會克隆一份棧上的地址,然後將 Rc 的引用計數加 1,現在 v 和 v2 都持有相同的一份數據,這就是為什麼它被稱為引用計數指針。

但是 Rc 也有限制,Rc 內部的數據是不可變的,你可以使用內部可變性可以解決這個問題。

每當有一個共享者超出作用域,引用計數就會減 1,讓引用計數減到 0 時,整個堆內存就會被釋放。

Send 和 Sync

Rust 有一些特殊的標記特徵,例如 Send 和 Sync。

如果一個類型實現了 Send,那就意味著數據可以從一個線程移動到另一個線程。

如果一個類型實現了 Sync,多個線程就可以使用引用去共享該數據。

Rc 沒有實現 Send 和 Sync。假設兩個線程在某個時間點同時擁有對某數據的引用,並且同時對該引用進行克隆。兩個線程同時更新引用計數就會引發線程安全問題。

Arc

如果你真的想要在線程間共享數據,你應該使用 原子 引用計數指針 Arc

Arc 的工作方式幾乎和 Rc 相同,只是引用計數的更新是原子性的,它是線程安全的。但是原子操作會帶來一些微小的性能損耗。如果你只需要在單線程內共享數據,使用 Rc 就夠了。

默認情況下 Arc 也是不變的,如果你想讓數據是可變的,你可以使用 Mutex

// Arc<Mutex<T>>

let data: Arc<Mutex<i32>> = Arc::new(Mutex::new(0));

現在即使有兩個線程嘗試同時修改數據,他們需要首先獲取鎖,同時只有有一個線程能拿到鎖,因此只能由一個線程修改數據。

特徵對象

實現了特徵的實例被稱為特徵對象。

下面列舉了將一種具體類型轉化為特徵對象的方法:

#![allow(unused)]
fn main() {
use std::io::Write;

let mut buffer: Vec<u8> = vec![];
let w: &mut dyn Write = &mut buffer;
}

第一個例子中,轉化發生在為變量 w 賦值時

fn main() {
    let mut buffer: Vec<u8> = vec![];
    writer(&mut buffer);
}

fn writer(w: &mut dyn Write) {
    // ...
}

第二個例子中,轉化發生在將具體類型變量傳遞給接受特徵對象的函數時

這兩個例子裡 Vec<u8> 類型的變量都被轉化為實現了 Write 的特徵對象。

Rust 用胖指針表示一個特徵對象。該胖指針由兩個普通指針組成,佔用 2 個機器字長。

  • 第一個指針指向值,這裡就是 Vec<u8>
  • 另一個指針指向 vtable (虛表)。

vtable 在編譯時生成,被所有相同類型的對象共享。vtable 包含了實現 Writer 必須實現的方法的指針。當你在調用特徵對象的方法時,Rust 自動使用 vtable 找到對應的方法。

注意:dyn Write 也是動態大小類型,因此我們總是使用它的引用,即 &dyn Write

我們能把 Vec<u8> 轉換成特徵對象是因為標準庫已經為它實現了 Write 特徵。

impl Write for Vec<u8>

Rust 不僅能將普通引用轉化為特徵對象,rust 也能將智能指針轉換為特徵對象:

// Box
use std::io::Write;

let mut buffer: Vec<u8> = vec![];
let w: Box<dyn Write> = Box::new(buffer);
// Rc
use std::io::Write;
use std::rc::Rc;

let mut buffer: Vec<u8> = vec![]

let mut w: Rc<dyn Write> = Rc::new(buffer);

無論是普通引用還是智能指針,在轉換髮生的時候,Rust 只是添加了適當的 vtable 指針,把原始指針轉換為了一個胖指針。

函數指針

函數指針只需要一個 usize 去存儲函數的地址。

test_func 是一個會返回 bool 的函數,我們可以把它存在了一個變量裡。

fn main() {
    let f: fn() -> bool = test_func;
}

fn test_func() -> bool {
    true
}

閉包

Rust 沒有具體的閉包類型,它制定了 3 個特徵 FnFnMutFnOnce

FnOnce

首先是 FnOncecreate_closere 函數返回了一個實現 FnOnce 的對象

fn main() {
    let c = create_closure();
}

fn create_closure() -> impl FnOnce() {
    let name = String::from("john");
    || {
        drop(name);
    }
}

在函數體內部我們創建了一個局部變量 name,它是字符串類型,在棧上佔據 3 個 usize ,接著又創建了一個閉包,閉包可以捕獲函數內的局部變量。在閉包內部,我們 drop 了 name。

FnOnce 只是一個特徵,它只定義了一個對象的行為或方法。Rust 內部會使用結構體表示閉包,它會根據閉包捕獲的變量創建對應的結構體,併為該結構體實現最合適的特徵

struct MyClosure {
    name: String
}

impl FnOnce for MyClosure {
    fn call_once(self) {
        drop(self.name)
    }
}

FnOnce 特徵的真實函數簽名比較複雜,這裡只展示一個簡化版本。

結構體內部只有一個 name 字段,是閉包從 create_closure 函數內部捕獲而來,call_onceFnOnce 特徵必須實現的方法。因為閉包對應的結構體只有一個 String 類型字段,所以他的內存佈局和 String 一樣。

注意 call_once 函數的參數,他需要一個 self ,這意味著 call_once 只能調用一次。原因也很簡單,如果我們調用兩次這個閉包,拿他就會 drop name 兩次。

FnMut

在這個例子裡,我們創建了一個可變的閉包:

let mut i: i32 = 0;

let mut f = || {
    i += 1;
};

f();
f();
println!("{}", i); // 2

這個閉包的類型是 FnMut ,因為我們在閉包裡嘗試修改變量 i 。因此該閉包生成的結構體中將會有一個對變量 i 的可變引用,call_mut 方法也需要一個對 self 的可變引用:

struct MyClosure {
    i: &mut i32
}

impl FnMut for MyClosure {
    fn call_mut(&mut self) {
        *self.i += 1;
    }
}

如果你在閉包 f 改為不可變的:

let f = || {
    i += 1;
};

就會編譯失敗:

error[E0596]: cannot borrow `f` as mutable, as it is not declared as mutable
  --> src/main.rs:16:5
   |
12 |     let f = || {
   |         - help: consider changing this to be mutable: `mut f`
13 |         i += 1;
   |         - calling `f` requires mutable binding due to mutable borrow of `i`
...
16 |     f();
   |     ^ cannot borrow as mutable
For more information about this error, try `rustc --explain E0596`.

錯誤信息提示我們,該閉包需要設為可變的

Fn

最後是 Fn 特徵:

fn create_closure() {
    let msg = String::from("hello");

    let my_print = || {
        println!("{}", msg);
    };

    my_print();
    my_print();
}

在這個例子裡,我們的閉包只是打印了一下它捕獲到的 msg 變量,print 宏接受的是變量的引用,所以 Rust 會自動為閉包實現 Fn 特徵:

struct MyClosure {
    msg: &String,
}

impl Fn for MyClosure {
    fn call(&self) {
        println!("{}", self.msg);
    }
}

生成的結構體內部只有一個對 msg 的引用。call 方法只需要一個 self 的引用,因此這個閉包能夠被多次調用。

move

這個例子中我們將使用和剛剛相同的閉包,只不過是用一個函數去返回:

fn create_closure() -> impl Fn() {
    let msg = String::from("hello");

    || {
        println!("{}", msg);
    }
}

但是這樣會編譯錯誤:

error[E0597]: `msg` does not live long enough
  --> src/main.rs:30:24
   |
29 |     || {
   |     -- value captured here
30 |         println!("{}", msg);
   |                        ^^^ borrowed value does not live long enough
31 |     }
32 | }
   | -- borrow later used here
   | |
   | `msg` dropped here while still borrowed

For more information about this error, try `rustc --explain E0597`.

錯誤信息提示我們,變量 msg 的生命週期可能比閉包短。

現在回想一下閉包的內存佈局,閉包的結構體內部只有一個對 msg 的引用。所以當函數調用結束後,它的棧幀將被釋放,閉包就不能再引用到該函數棧幀裡的局部變量。

Rust 希望我們使用 move 關鍵字去明確表示我們想讓閉包拿走閉包捕獲到的變量的所有權

fn create_closure() -> impl Fn() {
    let msg = String::from("hello");

    move || {
        println!("{}", msg);
    }
}

當我們使用 move 之後,閉包的結構體就不再是引用,而是字符串本身。

struct MyClosure {
    msg: String,
}

impl Fn for MyClosure {
    fn call(&self) {
        println!("{}", self.msg);
    }
}

捕獲多個變量

到目前為止,我們的閉包還只是捕獲一個變量,在這個例子裡閉包捕獲了兩個對象,一個字符串和一個 Vec:

fn create_closure() -> impl Fn() {
    let msg = String::from("hello");
    let v: Vec<i32> = vec![1, 2];

    move || {
        println!("{}", msg);
        println!("{:?}", v);
    }
}

它的結構體大致如下:

struct MyClosure {
    msg: String,
    v: Vec<i32>,
}

impl Fn for MyClosure {
    fn call(&self) {
        println!("{}", self.msg);
        println!("{:?}", self.v);
    }
}

它的內存佈局和結構體的一樣,並沒有什麼特殊的。

這個模式在其他地方也遵循,比如 異步生態中大量使用的 Future 特徵。在內存中編譯器會使用枚舉表示實際的對象,併為這個枚舉實現 Future 特徵。這裡不會詳細講解 Future 的實現細節,我提供了一個鏈接,視頻裡詳細的解釋了異步函數的實現細節。

資料

學習順序

Rust 是一個學習曲線比較陡峭的語言,即使有其他語言基礎,如果沒有先讀書,而是直接上,那在 compile 階段就會有很多挫折並且無法理解。以下是我覺得對已經有其他語言基礎的人,用這樣的學習順序是不錯的

  1. Rust 語言之旅:1 - 3 天就可以走完,並且因為是使用 playground,可以同時改改他的範例額外觀察一些自己有興趣的行為。走完之後大概會對於 Rust 與其他語言的差別有些感覺
  2. Rust book:Offical 的教學文,雖然寫的不算瑣碎,但若一開始就看這個可能還是會讓人失去耐心,畢竟一次會累績接收太多新東西。如果已經有了步驟 1,對於 Rust 跟其他語言的異同有感覺,那很多部分就可以參照其他本來就會的語言,因此有一個立足點,比較不會太挫折並失去耐心。不一定要一行一行看的很仔細,因為其實畢竟光看也會真的懂,所以就只是大概知道有哪些東西有個印象就行。可能會花到一週以上的時間,取決於看得多仔細以及多久失去耐心… 和下一步的 Rust by Example 順序可能可以調換,看你的習慣是比較喜歡讀書還是看 code…,如果很不愛讀文字的話甚至也可以跳過 Rust book,直接從 Rust by Example 開始也可以,如果有 Example 看不懂的地方再來翻翻看 Rust book
  3. Rust by Example:一些基礎 pattern 的範例,可以熟悉 Rust 語法和他的一些特別設計,尤其如 EnummatchClosure… 等等其他語言可能也有,但 Rust 卻在其上花更多功夫的部分。全部大概 3 天以內可以看完。如果有無法理解的地方,可能還是要回去翻 Rust book

到這裡就結束了讀書階段,總共花了 1~2 週的時間,往下就是練習了

  1. Chest sheet:可以開始上工了,學習語言這種東西是沒辦法只用看的,開始著手寫 project 才能真的學會,寫的過程就可以快速用這個 Chest sheet 查看語法,大部分在 Rust by Example 介紹到的 pattern 都有被以一行簡潔的收錄在 Chest sheet,反過來說,若看了 Chest sheet 還是有疑惑,可以回去翻 Rust by Example
  2. rustlings:Rust 官方提供的練習,可以開始練習小程式,範圍有可能是單一 Rust by Example,也有可能是複合。其實 rustlings 基本上就是 Rust by Example,只是之前 Rust by Example 你可能就只是光看,透過 rustlings 你可以真的動手寫一次,過程中 Chest sheet 就是好幫手
  3. Rust Algorithm Club:基礎演衣料結構和算法的實作
  4. Rust cookbook:完成之前的步驟之後,你基本上已經可以用 Rust 完成大部分的需求,但可以進一步再讀這個 Rust cookbook。他是官方收集了常用的情境,示範最專業的寫法。在往後你的實際專案中,你的程式的需求一定都用得到這些東西,也就是說當成為一個職業 Rust developer,Chest sheetRust by ExampleRust cookbook 就是三個開在旁邊隨時參考的東西。其實,如果是其他語言,當想找什麼語法我們可能都會選擇直接 Google,然後就會看到吐出 Stack Overflow 的結果可以直接參考,不過可能 Rust 是一個相對新的語言,加上他的學習曲線比較陡峭,所以 Stack Overflow 的回答可能會出現不太正確或者過於模糊的狀況,所以才建議從官方資源出發,扎實一點的學,往後就可以更有能力判斷別人的回答是對是錯。也不用全看啦,瀏覽一下他有哪些範例,然後挑幾個有興趣看一下就可以,之後真的開始寫專案,要來複製貼上的時候,再來把他看懂就可以
  5. 另外也有非官方的練習如 Exercism 提供更進階的題目。LeetCode 也有 Rust 啦,不過他畢竟主要是 for 面試情境,所以是以思考演算法為主要導向的,因此用高階一點的語言去刷比如 Python, Java 還是比較適合的。Rust 作為一個 system programming language,直接用它來開始寫 system application 就很好
  6. 可以開始寫完整的 project 了,如果沒有主題的話,可以從 Rust book 建議的 開始

Chest sheet

Chest sheet

Reference

Data Type

Basic

  • integer

    Length Signed Unsigned 8-bit i8 u8 16-bit i16 u16 32-bit i32(default) u32 64-bit i64 u64 128-bit i128 u128 arch isize usize

  • float: f32, f64(default)

  • bool

  • char: Fixed 4 bytes in size and represents a Unicode Scalar Value

Advanced

  • Tuple: () Fixed length. Group different types.
fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}
#![allow(unused)]
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup; // it's copying, not moving because it's on stack
}
#![allow(unused)]
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
}
  • Array: [] Same type. Fixed length
#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];

let a: [i32; 5] = [1, 2, 3, 4, 5];
let a = [3; 5]; //[3 ,3 ,3, 3, ,3]

let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}

往下是我自己補充 Chest sheet 中沒有的,或者一些比較難懂的概念

Borrowing

Rust 的 *& 在一開始不建議直接用 C 的方式來理解,而建議理解為 & 表達的是 借用,而不是 取址。 雖然其實基礎上就都是 reference,所以以底層來說實際上跟 C 差異不大,只是語法上如果直接想成跟 C 一樣,那會有好些 compile error 無法順利理解。

struct Foo {
    x: i32,
}

fn do_something(f: Foo) {
    println!("{}", f.x);
    // f is dropped here
}

fn main() {
    let mut foo = Foo { x: 42 };
    let f = &mut foo;

    // FAILURE: do_something(foo) would fail because
    // foo cannot be moved while mutably borrowed

    // FAILURE: foo.x = 13; would fail here because
    // foo is not modifiable while mutably borrowed

    f.x = 13;
    // f is dropped here because it's no longer used after this point
    
    println!("{}", foo.x);
    
    // this works now because all mutable references were dropped
    foo.x = 7;
    
    // move foo's ownership to a function
    do_something(foo);
}
  • Rust 只允許 一個 mut reference 或者 多個 unmut reference,但不會同時發生
  • 一個 reference 絕對不能活得比它的擁有者還長

對於借用而來的變數,操作時會使用到 *,雖然也叫做 dereferencing,但在 rust 來說一樣是要用 ownership 的概念來準確理解。

fn main() {
    let mut foo = 42;
    let f = &mut foo;
    let bar = *f; // get a copy of the owner's value
    *f = 13;      // set the reference's owner's value
    println!("{}", bar);
    println!("{}", foo);
}

注意 let bar = *f 是讓 bar 得到 f 的值的複製品,前提是 f 的型別有 Copy 屬性。

Example

Borrow checker 其實是個大魔王,他非常嚴格,可以寫寫這一題例子就會更有感覺 [LeeCode] #19 Remove Nth Node From End of List


Lifetime

Lifetime 就是為了 borrow checker 而存在,確保一個 reference 一定不會 refer 到一塊已經死掉的實體。大部分的狀況都 Elision,編譯器會幫忙補上,但編譯器無法判斷的狀況自然就要自己寫

// `print_refs` takes two references to `i32` which have different
// lifetimes `'a` and `'b`. These two lifetimes must both be at
// least as long as the function `print_refs`.
fn print_refs<'a, 'b>(x: &'a i32, y: &'b i32) {
    println!("x is {} and y is {}", x, y);
}

// A function which takes no arguments, but has a lifetime parameter `'a`.
fn failed_borrow<'a>() {
    let _x = 12;

    // ERROR: `_x` does not live long enough
    let y: &'a i32 = &_x;
    // Attempting to use the lifetime `'a` as an explicit type annotation 
    // inside the function will fail because the lifetime of `&_x` is shorter
    // than that of `y`. A short lifetime cannot be coerced into a longer one.
}

fn main() {
    // Create variables to be borrowed below.
    let (four, nine) = (4, 9);
    
    // Borrows (`&`) of both variables are passed into the function.
    print_refs(&four, &nine);
    // Any input which is borrowed must outlive the borrower. 
    // In other words, the lifetime of `four` and `nine` must 
    // be longer than that of `print_refs`.
    
    failed_borrow();
    // `failed_borrow` contains no references to force `'a` to be 
    // longer than the lifetime of the function, but `'a` is longer.
    // Because the lifetime is never constrained, it defaults to `'static`.
}

fn failed_borrow<'a>() 代表 'a 這個 lifetime 要 >= failed_borrow() 這個 funtcion 的 lifetime 但 main() 裡面呼叫到 failed_borrow() 的時候,沒有指定 'a 是什麼,那就預設 'a 就是 'static,而 'static 這個 lifetime 一定 >= failed_borrow() 的 lifetime

fn print_refs<'a, 'b>(x: &'a i32, y: &'b i32) 規範 print_refs() 這個 function 的 lifetime,一定要 <= 'a,'b 這兩個 lifetime,也就是他的兩個參數的 lifetime

Coercion

#![allow(unused)]
fn main() {
// Here, Rust infers a lifetime that is as short as possible.
// The two references are then coerced to that lifetime.
fn multiply<'a>(first: &'a i32, second: &'a i32) -> i32 {
    first * second
}
}

first second 兩個參數的 lifetime 不見得相同,但會取其小者,有就是 multiply() 這個 function 的 lifetime 一定要小於等於其兩個參數之中 lifetime 更小的那個

static

  • 一個靜態變量是在編譯時間就被產生的記憶體資源,它從程式一開始就存在,直到結束

  • 一定要明確的表示型別

  • 永遠不會被 drop

  • 如果靜態生命週期資源包含了 reference,那它們必須都得是 static

  • 修改靜態變量本質上就是危險的,因為任何人在任何地方都可以存取它們,而這有可能會造成 data racing

  • Rust 允許使用 unsafe { ... } 操作一些編譯器無法確保的記憶體行為


Collections- string

utf-8,有 1-4 個 bytes 的可變長度

fn main() {
    let a = "hi 🦀";
    println!("{}", a.len());
    let first_word = &a[0..2]; // 2 bytes => 2 Eng chars
    let second_word = &a[3..7]; // 4 bytes => 1 emoji
    // let half_crab = &a[3..5]; FAILS
    // Rust does not accept slices of invalid unicode characters
    println!("{} {}", first_word, second_word);
}
7
hi 🦀

因為可變長度的關係,查找字元時無法快速地以 O(1) 常數時間用索引完成,例如以 my_text[3] 取得第 4 個元)。取而代之的是必定得迭代整個 utf-8 位元序列,才有辦法知道各個 char 的開始點,所以是 O(n) 線性時間

push_str, +, to_uppercase, to_lowercase, trim, replace, concat, join

fn main() {
    let mut helloworld = String::from("hello");
    helloworld.push_str(" world");
    helloworld = helloworld + "!";
    println!("{}", helloworld);
    println!("{}", helloworld.to_uppercase());
    println!("{}", helloworld.trim()); // 切除空白
    println!("{}", helloworld.replace("world", "taiwan"));

    let helloworld = ["hello", " ", "world", "!"].concat();
    let abc = ["a", "b", "c"].join(",");
    println!("{}", helloworld);
    println!("{}",abc);
}
hello world!
HELLO WORLD!
hello world!
hello taiwan!
hello world!
a,b,c

to_string, parse

fn main() -> Result<(), std::num::ParseIntError> {
    let a = 42;
    let a_string = a.to_string();
    let b = a_string.parse::<i32>()?;
    println!("{} {}", a, b);
    Ok(())
}

format!

#![allow(unused)]
fn main() {
format!("Hello");                 // => "Hello"
format!("Hello, {}!", "world");   // => "Hello, world!"
format!("The number is {}", 1);   // => "The number is 1"
format!("{:?}", (3, 4));          // => "(3, 4)"
format!("{value}", value=4);      // => "4"
format!("{} {}", 1, 2);           // => "1 2"
format!("{:04}", 42);             // => "0042" with leading zeros
format!("{:#?}", (100, 200));     // => "(
                                  //       100,
                                  //       200,
                                  //     )"
}

more…

include_str

如果你有一些非常長的文字,可以考慮使用 marco include_str! 將 string 從 file 讀到程式裡

#![allow(unused)]
fn main() {
let hello_html = include_str!("hello.html");
}

chars

Rust 提供了一個方法可以取得一個 utf-8 位元組的字元向量,它的型別是 char。一個 char 的大小永遠是 4 bytes

fn main() {
    // collect the characters as a vector of char
    let chars = "hi 🦀".chars().collect::<Vec<char>>();
    println!("{}", chars.len()); // should be 4
    // since chars are 4 bytes we can convert to u32
    println!("{}", chars[3] as u32);
}

stringchars() 方法將 string 分離為各個有意義的 character,並放入空間一律為 4 bytes 的 char 型別中,串成一個 Vec 回傳。

#![allow(unused)]
fn main() {
for c in my_str.chars() { 
    // do something with `c`
}

for (i, c) in my_str.chars().enumerate() {
    // do something with character `c` and index `i`
}
}

Collections- Vector

  • can only store values of the same type
  • puts all the values next to each other in memory
fn main() {
    // Iterators can be collected into vectors
    let collected_iterator: Vec<i32> = (0..10).collect();
    println!("Collected (0..10) into: {:?}", collected_iterator);

    // The `vec!` macro can be used to initialize a vector
    let mut xs = vec![1 i32, 2, 3];
    println!("Initial vector: {:?}", xs);

    // Insert new element at the end of the vector
    println!("Push 4 into the vector");
    xs.push(4);
    println!("Vector: {:?}", xs);

    // Error! Immutable vectors can't grow
    collected_iterator.push(0);
    // FIXME ^ Comment out this line

    // The `len` method yields the number of elements currently stored in a vector
    println!("Vector length: {}", xs.len());

    // Indexing is done using the square brackets (indexing starts at 0)
    println!("Second element: {}", xs[1]);

    // `pop` removes the last element from the vector and returns it
    println!("Pop last element: {:?}", xs.pop());

    // Out of bounds indexing yields a panic
    println!("Fourth element: {}", xs[3]);
    // FIXME ^ Comment out this line

    // `Vector`s can be easily iterated over
    println!("Contents of xs:");
    for x in xs.iter() {
        println!("> {}", x);
    }

    // A `Vector` can also be iterated over while the iteration
    // count is enumerated in a separate variable (`i`)
    for (i, x) in xs.iter().enumerate() {
        println!("In position {} we have value {}", i, x);
    }

    // Thanks to `iter_mut`, mutable `Vector`s can also be iterated
    // over in a way that allows modifying each value
    for x in xs.iter_mut() {
        *x *= 3;
    }
    println!("Updated vector: {:?}", xs);
}

Collections- HashMap

All of the keys must have the same type, and all of the values must have the same type. Any type that implements the Eq and Hash traits can be a key in HashMap. This includes:

  • bool (though not very useful since there is only two possible keys)
  • int, uint, and all variations thereof
  • String and &str
  • You can easily implement Eq and Hash for a custom type with just one line: #[derive(PartialEq, Eq, Hash)]
#![allow(unused)]
fn main() {
use std::collections::HashMap;

let mut scores = HashMap::new();
// `HashMap::insert()` returns `None`
// if the inserted value is new, `Some(value)` otherwise
scores.insert(String::from("Blue"), 10);
let team_name = String::from("Blue");
let score = scores.get(&team_name);

scores.entry(String::from("Yellow")).or_insert(50); // Only Inserting a Value If the Key Has No Value

for (key, value) in &scores {
    println!("{}: {}", key, value);
}

contacts.remove(&"Yellow"); 

let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let mut scores: HashMap<_, _> =
    teams.into_iter().zip(initial_scores.into_iter()).collect();

// `HashMap::iter()` returns an iterator that yields 
// (&'a key, &'a value) pairs in arbitrary order.
for (name, &number) in teams.iter() {
    println!("Calling {}: {}", name, call(number)); 
}
}

HashSet

Consider a HashSet as a HashMap where we just care about the keys ( HashSet<T> is, in actuality, just a wrapper around HashMap<T, ()>). A HashSet’s unique feature is that it is guaranteed to not have duplicate elements. That’s the contract that any set collection fulfills. HashSet is just one implementation.


Trait

  • 可理解為其他語言(如 python)的 Interface
  • Trait 裡面的 method 可以有 default 實作,但他無法操作 struct 的 inner fields,也就是說一個 trait 只能定義需要有哪些 method,無法定義需要有哪些成員
  • trait 可以繼承另一個 trait (Supertraits):trait LoudNoiseMaker: NoiseMaker。代表 LoudNoiseMaker 也要有 NoiseMaker 規範的所有 method

Handling Unsized Data

When we want to store them within another struct, traits obfuscate the original struct thus it also obfuscates the original size. Unsized values being stored in structs are handled in two ways in Rust:

  • generics - Using parameterized types effectively create struct/functions known types and thus known sizes. 即使用 impl Trait
  • indirection - Putting instances on the heap gives us a level of indirection that allow us to not have to worry about the size of the actual type and just store a pointer to it. 即使用 Box<dyn Trait>

Trait bound (a.k.a. impl Trait)

fn animal_talk(a: impl Animal) {
  a.talk();
}
/* same as
fn animal_talk<T>(a: T)
where
    T: Animal
{
  a.talk();
}
*/

fn main() {
  let c = Cat{};
  let d = Dog{};
 
  animal_talk(c);
  animal_talk(d);
}

impl Animal There is no & there. impl here makes the compiler determine the type at the compile time. One that takes Dog and another that takes Cat. This is called monomorphization and will not have any runtime overhead.

For example,

#![allow(unused)]
fn main() {
fn animal () -> impl Animal {
  if (is_dog_available()) {
    return Dog {};
  }
  Cat {}
}
}

It fails! because, the types here are determined at the compile time (static dispatch) .

#![allow(unused)]
fn main() {
fn animal() -> Box<dyn Animal> {
  if (is_dog_available()) {
    return Box::new(Dog {});
  } 
    
  Box::new(Cat {})
}
}

This works!

Static vs Dynamic Dispatch (a.k.a. dyn Trait)

&dyn NoiseMaker is a trait object. It represents a pointer to the concrete type and a pointer to a vtable of function pointers. (Box<dyn Animal>, Rc<dyn Animal> are also trait Objects.) A trait object is what allows us to indirectly call the correct methods of an instance. A trait object is a struct that holds the pointer of our instance with a list of function pointers to our instance’s methods. This list of functions is known in C++ as a vtable.

struct SeaCreature {
    pub name: String,
    noise: String,
}

impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
}

trait NoiseMaker {
    fn make_noise(&self);
}

impl NoiseMaker for SeaCreature {
    fn make_noise(&self) {
        println!("{}", &self.get_sound());
    }
}

fn static_make_noise(creature: &SeaCreature) {
    // we know the real type
    creature.make_noise();
}

fn dynamic_make_noise(noise_maker: &dyn NoiseMaker) {
    // we don't know the real type
    noise_maker.make_noise();
}

fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris"),
        noise: String::from("blub"),
    };
    static_make_noise(&creature);
    dynamic_make_noise(&creature);
}

Dynamic dispatch is slightly slower because of the pointer chasing to find the real function call.

Derive

#![allow(unused)]
fn main() {
#[derive(PartialEq, PartialOrd)]
}

讓編譯器自動幫忙補上一些基本的 trait,如下

  • Comparison traits: Eq, PartialEq, Ord, PartialOrd.
  • Clone, to create T from &T via a copy. Introduce .clone().
  • Copy, to give a type ‘copy semantics’ instead of ‘move semantics’. Introct .copy().
  • Hash, to compute a hash from &T.
  • Default, to create an empty instance of a data type.
  • Debug, to format a value using the {:?} formatter.
  • Add, Sub, inctoduce +, - operators.
  • Drop, you can override .drop()
  • Iterator

Rc, Arc, Refcell, Mutex

Rc. Reference Count. 用來裝一個 (smart) pointer,如此便提供了 clone() 的能力,也就是兩個不同的 pointer 指向同一塊資料。 (為什麼叫 smart pointer 可以參考這裡) 為什麼需要這個?記得 Rust 的理念,ownership 基本上只有一個,所以不是 move 就是 borrow,如果不是 move 也不是 borrow,那隻能 copy,那實際上就是兩塊獨立的資料自然可以各有各的 ownership。藉由單一擁有者,就可以透過該擁有者的作用域(scope),在正確的時間做 drop 回收記憶體。 但若牽扯到指標,就變成要確保已經沒有任何指標指到某一塊資料,才可以 drop,所以需要導入 Rc,只要是透過 Rc clone 的指標都會被記錄,確保在 count 歸零時才把記憶體釋放。

use std::rc::Rc;

struct Pie;

impl Pie {
    fn eat(&self) {
        println!("tastes better on the heap!")
    }
}

fn main() {
    let heap_pie = Rc::new(Pie);
    let heap_pie2 = heap_pie.clone();
    let heap_pie3 = heap_pie2.clone();

    heap_pie3.eat();
    heap_pie2.eat();
    heap_pie.eat();

    // all reference count smart pointers are dropped now
    // the heap data Pie finally deallocates
}

Refcell 用來裝一個 (smart) pointer,提供 borrow mutable/immutable references 的能力,好處是 Refcell 負責確保 Only one mutable reference OR multiple immutable references, but not both! 為什麼需要這個?因為 Rc 只提供了複製指標的能力,讓我們可以有複數個指標指向同一塊資料,因此也負責確保了 drop 該資料記憶體的時機。但他沒有確保 mutablility 的部分,同時有兩個活著的 mutable references 指向同一塊資料是危險的。

如下面這例子,ferrissarah 的 pie 其實是指向同一塊資料,並非擁有各自的 pie

use std::cell::RefCell;
use std::rc::Rc;

struct Pie {
    slices: u8,
}

impl Pie {
    fn eat_slice(&mut self, name: &str) {
        println!("{} took a slice!", name);
        self.slices -= 1;
    }
}

struct SeaCreature {
    name: String,
    pie: Rc<RefCell<Pie>>,
}

impl SeaCreature {
    fn eat(&self) {
        // use smart pointer to pie for a mutable borrow
        let mut p = self.pie.borrow_mut();
        // take a bite!
        p.eat_slice(&self.name);
    }
}

fn main() {
    let pie = Rc::new(RefCell::new(Pie { slices: 8 }));
    // ferris and sarah are given clones of smart pointer to pie
    let ferris = SeaCreature {
        name: String::from("ferris"),
        pie: pie.clone(),
    };
    let sarah = SeaCreature {
        name: String::from("sarah"),
        pie: pie.clone(),
    };
    ferris.eat();
    sarah.eat();

    let p = pie.borrow();
    println!("{} slices left", p.slices);
}

如果說兩個 pointer 會由不同的 thread 擁有,Rc 就要換成使用 Arc

且如果說會跨 thread,Refcell 就變成要使用 Mutex。所以經常看到的組合就是

  • within thread: Rc<RefCell<...>>
  • across thread: Arc<Mutex<...>>

此時可能會產生一個疑問,在跨 thread 狀況下有 data racing 的風險這我們知道,因此需要導入 Arc 和 Mutex 合理;但若為 single thread 的狀況,需要 Rc 來知道 drop 時機可以理解,不過為何需要 Refcell?既然只有 single thread,怎麼可能會有同一時間存在兩個 mutable reference 的狀況? 會有這個疑問可能是忽略了 Rust 畢竟還是一個與 C 相同層級的語言,他是可以提供記憶體位址等級的操作的。也就是說你可以拿到一個 instance 的 address,並透過該 address 對該 instance 操作。問題是,若透過這樣的方式,這些操作就是不是 compiler 可以追蹤到並且介入規範的,自由當然是自由,但風險就要自負。這種操作在 Rust 叫做 Unsafe,比如 dereferencing a raw pointer 操作,只要用 Unsafe block 包起來,Rust compiler 就會不管你裡面的操作。因此,如果我們對於 pointer 這種透過指標去操作一塊資料的行為,都透過 Rc, RefCell 這種 wrapper,那就可以讓 compiler 介入幫我們確保我們對於指標的使用安全,順帶一提,可以想見這個 wrapper 的內部實作終究還是會有 Unsafe block,只是說他在外包了一層,加入了一些 metadata,讓編譯器可以藉由這些 metadata 此來幫助我們追蹤和確保。

Conversion

透過 impl FromInto 這兩個 traits,可以讓你的 type 擁有 ::from()::into() 的方法來做型別轉換

use std::convert::From;

#[derive(Debug)]
struct Number {
    value: i32,
}

impl From<i32> for Number {
    fn from(item: i32) -> Self {
        Number { value: item }
    }
}

fn main() {
    let num = Number::from(30);
    println!("My number is {:?}", num);
}
use std::convert::From;

#[derive(Debug)]
struct Number {
    value: i32,
}

impl From<i32> for Number {
    fn from(item: i32) -> Self {
        Number { value: item }
    }
}

fn main() {
    let int = 5;
    // Try removing the type declaration
    let num: Number = int.into();
    println!("My number is {:?}", num);
}

TryFrom/TryInto traits are used for fallible conversions, and as such, return Results.

use std::convert::TryFrom;
use std::convert::TryInto;

#[derive(Debug, PartialEq)]
struct EvenNumber(i32);

impl TryFrom<i32> for EvenNumber {
    type Error = ();

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        if value % 2 == 0 {
            Ok(EvenNumber(value))
        } else {
            Err(())
        }
    }
}

fn main() {
    // TryFrom

    assert_eq!(EvenNumber::try_from(8), Ok(EvenNumber(8)));
    assert_eq!(EvenNumber::try_from(5), Err(()));

    // TryInto

    let result: Result<EvenNumber, ()> = 8i32.try_into();
    assert_eq!(result, Ok(EvenNumber(8)));
    let result: Result<EvenNumber, ()> = 5i32.try_into();
    assert_eq!(result, Err(()));
}

Debug

use std::fmt; // Import the `fmt` module.

// Define a structure named `List` containing a `Vec`.
struct List(Vec<i32>);

impl fmt::Display for List {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // Extract the value using tuple indexing,
        // and create a reference to `vec`.
        let vec = &self.0;

        write!(f, "[")?;

        // Iterate over `v` in `vec` while enumerating the iteration
        // count in `count`.
        for (count, v) in vec.iter().enumerate() {
            // For every element except the first, add a comma.
            // Use the ? operator to return on errors.
            if count != 0 { write!(f, ", ")?; }
            write!(f, "{}", v)?;
        }

        // Close the opened bracket and return a fmt::Result value.
        write!(f, "]")
    }
}

fn main() {
    let v = List(vec![1, 2, 3]);
    println!("{}", v);
}

Module

  • 一個 Program 有一個 main.rs,裡面實作 main() 方法

  • 一個 Lib 有一個 root module lib.rs,可以再包含數個 module 作為 submodule

  • Program 或者 Lib 都稱為一個 crate,是一個 compilation unit,也可以對應到其他語言 package 的概念,是一個導入第三方函式庫的單位,透過 use 來導入 namespace

  • 一個

    (Sub)Module

    有兩個方式,取決於 code 大小,比如創一個 submodule foo

    • ./foo.rs:所有該 foo module 的 code 都在這個 rs file 裡
    • ./foo/mod.rs:foo 這個資料夾底下還可以有其他 rs file,他們合起來完整 foo 這個 module
  • access 一個 (sub)module 與 file path 強相關,並且有三個關鍵字可作為起點

    • crate - the root module of your crate
    • super - the parent module of your current module
    • self - the current module

所以一個 crate 可能就長這樣

├── lib.rs
├── main.rs
├── submod1.rs
└── submod2
    ├── file1.rs
    ├── file2.rs
    ├── mod.rs
    └── submod3
        ├── file3.rs
        └── mod.rs

除了 file 層級的 module 劃分,單一 rs 裡面也可以用 mod 定義自己的 submodule

share lib

$ rustc --crate-type=lib rary.rs
$ ls lib*
library.rlib

rary is actuaylly refer to the whole acssociated crate. It does not imply only build single rary.rs file.

To using a Library

// extern crate rary; // May be required for Rust 2015 edition or earlier
fn main() {
    rary::public_function();
}

Error Handling

Option` 和 `Result` 都有 `unwrap()` 和 `?` 可以使用,基本上一個 function 的 return 值建議就是從這兩者選其一,並且不建議 caller 直接使用 `unwarp()` 處理,因為這會造成 `panic

Option

map()Option 提供的方法,參數是一個 closure,他其實就是在這種情境中 match 語法的簡化,如下範例

#![allow(unused)]
fn main() {
// Chopping food. If there isn't any, then return `None`.
// Otherwise, return the chopped food.
fn chop(peeled: Option<Peeled>) -> Option<Chopped> {
    match peeled {
        Some(Peeled(food)) => Some(Chopped(food)),
        None               => None,
    }
}

// Cooking food. Here, we showcase `map()` instead of `match` for case handling.
fn cook(chopped: Option<Chopped>) -> Option<Cooked> {
    chopped.map(|Chopped(food)| Cooked(food))
}
/* instead of
fn cook(chopped: Option<Chopped>) -> Option<Cooked> {
    match chopped {
        Some(Chopped(food)) => Some(Cooked(food)),
        None => None
    }
}
*/
}

在串兩的都是 return Option 的 function 的時候,如果兩個 function 的 return Option 相同,那可以直接使用 ?;若不同,則要做轉換,但應換成使用 Optionand_then() 方法,因為若使用 map(),會多一層 Option,因為 map() 包含一個簡化是會幫你加上 Some() 裝起來

下面這個範例就是串 have_recipe()have_ingredients() 兩個 function

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)] enum Food { CordonBleu, Steak, Sushi }
#[derive(Debug)] enum Day { Monday, Tuesday, Wednesday }

fn have_ingredients(food: Food) -> Option<Food> {
    match food {
        Food::Sushi => None,
        _           => Some(food),
    }
}

fn have_recipe(food: Food) -> Option<Food> {
    match food {
        Food::CordonBleu => None,
        _                => Some(food),
    }
}

fn cookable_v2(food: Food) -> Option<Food> {
    have_recipe(food).and_then(have_ingredients)
    // instead of have_recipe(food).map(|f| have_ingredients(f))
    //   becauset his will return Option<Option<Food>>
}
}

as_ref(), as_mut()

#![allow(unused)]
fn main() {
pub const fn as_ref(&self) -> Option<&T>
}

Converts from &Option<T> to Option<&T>.

這東西的重要性在於可以產生一個被包在 Option 裡面的東西的 reference 而不用 take ownership

#![allow(unused)]
fn main() {
pub fn as_mut(&mut self) -> Option<&mut T>
}

Converts from &mut Option<T> to Option<&mut T>.

#![allow(unused)]
fn main() {
let mut x = Some(2);
match x.as_mut() {
    Some(v) => *v = 42,
    None => {},
}
assert_eq!(x, Some(42));
}

注意轉換過之後都還是包在 Option 裡面

Option, Box, Result 三種類型才有支持預設,其他要實作 AsRef trait

fn main() {
    let mut a = Some(Box::new(5));
    let p1 = a.as_ref(); // p 沒有 take ownership 喔. 5 的 ownership 還是在 a

    println!("{:?}", a);
    println!("{:?}", p1);
    
    let p11 = p1.unwrap();
    println!("p11:{:?}", p11); // 可以透過 p11 拿到 5
    println!("a:{:?}", a);
    
    // 以下是犯一些如果不使用 as_ref(),會遇到的 ownership 問題
    
    let p2 = &mut a;
    //let p22 = p2.unwrap(); // cannot move out of `*p2` which is behind a shared reference

    /*let p22 = match *p2 { // cannot move out of `p2.0` which is behind a mutable reference
        Some(v) => *v, // v: data moved here
        None => 0
    };*/
    
    let p22 = match *p2 {
        Some(ref v) => **v, // 處理上面錯誤的方法就是用 ref 來接
        None => 0
    };
    println!("p22:{:?} {:p}", p22, &p22);
    //println!("a:{:?}", a); // cannot borrow `a` as immutable because it is also borrowed as mutable
    // 這個時候 5 的 ownership 都還是在 a,所以當這裡要用 a 就會有這個衝突

    let p23 = match p2.take() { // 或者要把原本 a 的 ownership take 過來到 p2,p2 才有權把裡面的東西移去 v
        Some(v) => *v,
        None => 0
    };
    println!("p23:{:?} {:p}", p23, &p23);
    println!("a:{:?}", a); // 因為原本 a 的 ownership 已經被轉移去 p2,所以 a 已經變 None 自然也不再有衝突
}

Result

對於一個 module 而言,最完整的 erro propagation 做法為

  1. 定義自己的 error 型別,比如 enum MyError {},列舉各種可能產生的 error,同時也包含當使用到其他 module 時,各個其他 moduel 的 error 歸類到 MyError 中的其中一個可能值
  2. impl fmt::Display for MyError
  3. impl error::Error for MyError 實作 source() 方法,source() 方法使跨抽象層的狀況讓上層 module 也能取得更多其下下層 module 的 error 細節的可能性。也就是說,這一步可以不做,但不做的結果就是,假設 某A 使用你的 module,當你的 module 發生了 call 另一個 moduleB 時產生的該 moduleB 的 error 因此導致你的 moduel 也 report error 給 某A,某A 所能得到的唯一資訊就是你翻譯過的 error,無法進一步透過 source() 進到 moduleB 裡去提取更多資訊
  4. impl From<OtherModuleErrorType> for MyError? 可用。也可以不做,若不做就變成當要 call 其他 module 時,要用 method_provided_by_another_module().ok_or(MyError::SomeTranslatedError)? 來做轉換。相當於 ? 其實就是會去找 From 方法存不存在,若存在就會依據 From 的內容做轉換

下面範例

fn double_first(vec: Vec<&str>) -> i32 {
    let first = vec.first().unwrap(); // Generate error 1
    2 * first.parse::<i32>().unwrap() // Generate error 2
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    println!("The first doubled is {}", double_first(numbers));

    println!("The first doubled is {}", double_first(empty));
    // Error 1: the input vector is empty

    println!("The first doubled is {}", double_first(strings));
    // Error 2: the element doesn't parse to a number
}

假設這就是我們的 module,裡面用了其他兩個 moduel 的方法 first()parse(),他們都會產生各自定義的 error type。下面就是依據上述的步驟改寫的結果,我們定義了自己的 error type DoubleError 把他們包起來並個別翻譯

use std::error;
use std::error::Error as _;
use std::num::ParseIntError;
use std::fmt;

type Result<T> = std::result::Result<T, DoubleError>;

#[derive(Debug)]
enum DoubleError {
    EmptyVec,
    // We will defer to the parse error implementation for their error.
    // Supplying extra info requires adding more data to the type.
    Parse(ParseIntError),
}

impl fmt::Display for DoubleError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            DoubleError::EmptyVec =>
                write!(f, "please use a vector with at least one element"),
            // The wrapped error contains additional information and is available
            // via the source() method.
            DoubleError::Parse(..) =>
                write!(f, "the provided string could not be parsed as int"),
        }
    }
}

impl error::Error for DoubleError {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match *self {
            DoubleError::EmptyVec => None,
            // The cause is the underlying implementation error type. Is implicitly
            // cast to the trait object `&error::Error`. This works because the
            // underlying type already implements the `Error` trait.
            DoubleError::Parse(ref e) => Some(e),
        }
    }
}

// Implement the conversion from `ParseIntError` to `DoubleError`.
// This will be automatically called by `?` if a `ParseIntError`
// needs to be converted into a `DoubleError`.
impl From<ParseIntError> for DoubleError {
    fn from(err: ParseIntError) -> DoubleError {
        DoubleError::Parse(err)
    }
}

fn double_first(vec: Vec<&str>) -> Result<i32> {
    let first = vec.first().ok_or(DoubleError::EmptyVec)?;
    // Here we implicitly use the `ParseIntError` implementation of `From` (which
    // we defined above) in order to create a `DoubleError`.
    let parsed = first.parse::<i32>()?;

    Ok(2 * parsed)
}

fn print(result: Result<i32>) {
    match result {
        Ok(n)  => println!("The first doubled is {}", n),
        Err(e) => {
            println!("Error: {}", e);
            if let Some(source) = e.source() {
                println!("  Caused by: {}", source);
            }
        },
    }
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    print(double_first(numbers));
    print(double_first(empty));
    print(double_first(strings));
}

Multithreading

use std::sync::mpsc::{Sender, Receiver};
use std::sync::mpsc;
use std::thread;

static NTHREADS: i32 = 3;

fn main() {
    // Channels have two endpoints: the `Sender<T>` and the `Receiver<T>`,
    // where `T` is the type of the message to be transferred
    // (type annotation is superfluous)
    let (tx, rx): (Sender<i32>, Receiver<i32>) = mpsc::channel();
    let mut children = Vec::new();

    for id in 0..NTHREADS {
        // The sender endpoint can be copied
        let thread_tx = tx.clone();

        // Each thread will send its id via the channel
        let child = thread::spawn(move || {
            // The thread takes ownership over `thread_tx`
            // Each thread queues a message in the channel
            thread_tx.send(id).unwrap();

            // Sending is a non-blocking operation, the thread will continue
            // immediately after sending its message
            println!("thread {} finished", id);
        });

        children.push(child);
    }

    // Here, all the messages are collected
    let mut ids = Vec::with_capacity(NTHREADS as usize);
    for _ in 0..NTHREADS {
        // The `recv` method picks a message from the channel
        // `recv` will block the current thread if there are no messages available
        ids.push(rx.recv());
    }
    
    // Wait for the threads to complete any remaining work
    for child in children {
        child.join().expect("oops! the child thread panicked");
    }

    // Show the order in which the messages were sent
    println!("{:?}", ids);
}

exec

use std::io::prelude::*;
use std::process::{Command, Stdio};

static PANGRAM: &'static str =
"the quick brown fox jumped over the lazy dog\n";

fn main() {
    // Spawn the `wc` command
    let process = match Command::new("wc")
                                .stdin(Stdio::piped())
                                .stdout(Stdio::piped())
                                .spawn() {
        Err(why) => panic!("couldn't spawn wc: {}", why),
        Ok(process) => process,
    };

    // Write a string to the `stdin` of `wc`.
    //
    // `stdin` has type `Option<ChildStdin>`, but since we know this instance
    // must have one, we can directly `unwrap` it.
    match process.stdin.unwrap().write_all(PANGRAM.as_bytes()) {
        Err(why) => panic!("couldn't write to wc stdin: {}", why),
        Ok(_) => println!("sent pangram to wc"),
    }

    // Because `stdin` does not live after the above calls, it is `drop`ed,
    // and the pipe is closed.
    //
    // This is very important, otherwise `wc` wouldn't start processing the
    // input we just sent.

    // The `stdout` field also has type `Option<ChildStdout>` so must be unwrapped.
    let mut s = String::new();
    match process.stdout.unwrap().read_to_string(&mut s) {
        Err(why) => panic!("couldn't read wc stdout: {}", why),
        Ok(_) => print!("wc responded with:\n{}", s),
    }
}

If you’d like to wait for a process::Child to finish, you must call Child::wait, which will return a process::ExitStatus.

use std::process::Command;

fn main() {
    let mut child = Command::new("sleep").arg("5").spawn().unwrap();
    let _result = child.wait().unwrap();

    println!("reached end of main");
}

Argument parsing

use std::env;

fn increase(number: i32) {
    println!("{}", number + 1);
}

fn decrease(number: i32) {
    println!("{}", number - 1);
}

fn help() {
    println!("usage:
match_args <string>
    Check whether given string is the answer.
match_args {{increase|decrease}} <integer>
    Increase or decrease given integer by one.");
}

fn main() {
    let args: Vec<String> = env::args().collect();

    match args.len() {
        // no arguments passed
        1 => {
            println!("My name is 'match_args'. Try passing some arguments!");
        },
        // one argument passed
        2 => {
            match args[1].parse() {
                Ok(42) => println!("This is the answer!"),
                _ => println!("This is not the answer."),
            }
        },
        // one command and one argument passed
        3 => {
            let cmd = &args[1];
            let num = &args[2];
            // parse the number
            let number: i32 = match num.parse() {
                Ok(n) => {
                    n
                },
                Err(_) => {
                    eprintln!("error: second argument not an integer");
                    help();
                    return;
                },
            };
            // parse the command
            match &cmd[..] {
                "increase" => increase(number),
                "decrease" => decrease(number),
                _ => {
                    eprintln!("error: invalid command");
                    help();
                },
            }
        },
        // all the other cases
        _ => {
            // show a help message
            help();
        }
    }
}

References

From: A Po Author: Chris Chung Link: https://chungchris.github.io/2021/06/30/software/language/rust-note/ 本文章成功權歸作者所有,形式的轉載都請註明出處。

學 Rust 要有大局觀

Crash Rust

學習一種新的語言,首先需要了解的就是該語言的基本設計思路,工程架構特點,本文希望可以幫住大多數對 Rust 感興趣的同學快速進入具體工程開發,並掃清大部分除基本語法之外的障礙; 具體涉及到的主題包括安裝,運行,發佈,三方包引入等等;

Rust 安裝

install

安裝 Rust 非常簡單,只需要一條命令,但是注意部分機器 curl 版本可能導致命令執行失敗,比如樑小孩自己的開發機 ubuntu20.04 自帶的 curl 提示 ssl 443 錯誤,如果遇到的話,嘗試重新安裝 curl

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

check

執行 Rustup 命令, 注意如果是第一次全新安裝, 先執行 source $HOME/.cargo/env

root@ecs-x86 :~/docs/Rust/CrashRustIn2Hours# source $HOME/.cargo/env
root@ecs-x86 :~/docs/Rust/CrashRustIn2Hours# rustup
Rustup 1.24.2 (755e2b07e 2021-05-12)
...

Hello Rust!

接下來我們完成第一個 Rust 項目,功能只有一句打印輸出,但是大家在此章節應該學到的是如何編譯並運行 Rust 代碼程序;

編碼

創建第一個rs文件 (hello.rs),文件內容如下:

fn main() {
    println!("hello Rust!");
}

文件目錄結構簡單,代碼目錄下只有一個單獨的hello.rs文件如下:

root@ecs-x86 CrashRustIn2Hours# tree code/
code/
└── hello.rs
0 directories, 1 file

編譯 & 執行

Rust 的編譯器叫做rustc,編譯時直接後跟待編譯的rs文件即可, 不執行輸出文件名則生成同名的可執行文件, 可以直接執行輸出文件得到程序結果

root@ecs-x86 code# rustc hello.rs 
root@ecs-x86 code# tree 
.
├── hello
└── hello.rs
0 directories, 2 files
root@ecs-x86 code# ls -lhF
total 3.3M
-rwxr-xr-x 1 root root 3.3M Jun  2 13:32 hello*
-rw-rw-rw- 1 root root   42 Jun  2 13:29 hello.rs
root@ecs-x86 code# ./hello 
hello Rust!

到此你看到的是純手工開發流程,包括編譯,文件創建,目錄組織等等都需要自行維護,為了方便工程化,並且快速創建工程,Rust 提供了自己的自動化工具鏈cargo;

Hello Cargo!

cargo之於Rust猶如npm之於node, cargo可以幫助你維護包依賴關係,安裝三方包,自動編譯代碼,執行結果,完善debugrelease的各種部署需求;

cargo三板斧:

cargo init 自動創建工程,包括基本的配置文件,main.rs生成等等

root@ecs-x86 code# cargo init
     Created binary (application) package
root@ecs-x86 code# tree
.
├── Cargo.toml
└── src
    └── main.rs
1 directory, 2 filese

cargo run 自動編譯並執行; 注意,此時編譯選項均為 debug 模式,所以 target 目錄下只有一個 debug 目錄自動生成 (可執行文件亦在其中)

root@ecs-x86 code# cargo run
   Compiling code v0.1.0 (/root/docs/Rust/CrashRustIn2Hours/code)
    Finished dev [unoptimized + debuginfo] target(s) in 1.34s
     Running `target/debug/code`
Hello, world!
root@ecs-x86 code# tree -L 2
.
├── Cargo.lock
├── Cargo.toml
├── src
│   └── main.rs
└── target
    ├── CACHEDIR.TAG
    └── debug

cargo build 單獨編譯,如果需要單獨 build 可能是最終發佈二進製程序,此時一般附帶參數--release

root@ecs-x86 code# cargo build --release
   Compiling code v0.1.0 (/root/docs/Rust/CrashRustIn2Hours/code)
    Finished release [optimized] target(s) in 0.35s
root@ecs-x86 code# tree -L 2
.
├── Cargo.lock
├── Cargo.toml
├── src
│   └── main.rs
└── target
    ├── CACHEDIR.TAG
    ├── debug
    └── release
4 directories, 4 files

crate 和 module

crate是 rust 對外分發和代碼共享的單位,類似 jar 包,類似 so 庫,是三方庫的概念 •crate是編譯打包的概念 •crate的核心標誌就是一個單獨的Cargo.toml文件,是一個邏輯可編譯的功能庫 •cargo package可以在target/package/目錄下生成對應的*.crate打包文件 •module是模塊的概念,是代碼組織方式,類似於 c++ 的namespace, 類似於 golang 的package的概念 •module的核心標誌是語法層面的use <module_name>的導入和聲明 •module有三種文件組織方式 (假設建立一個叫做string_util的 module)1. 內嵌文件中使用mod string_util { ... }的方式進行定義, 內部可以包含任意多函數,結構體等等 2. 建立一個獨立文件名string_util.rs,內部無上面的顯式mod <module_name>聲明 3. 建立一個文件夾string_util內部包含一個mod.rs文件,還有其他submodule的話文件夾中一般使用第二種創建新文件 • 三者取其一,如果發現都沒有或者有多重情況定義同一個mod的時候 rustc 便會報錯.

關於crate的其他理解可以參考官方 doc 的部分描述:

A crate is a compilation unit in Rust. Whenever rustc some_file.rs is called, some_file.rs is treated as the crate file. If some_file.rs has mod declarations in it, then the contents of the module files would be inserted in places where mod declarations in the crate file are found, before running the compiler over it. In other words, modules do not get compiled individually, only crates get compiled.

Rust thinks in modules, not files. There is no such thing as file imports, Important concepts in the Rust Module System are packages, crates, modules, and paths

學習modulecrate的時候需要了解的基本前提知識如下:

rust 的pubilcprivate的作用域控制結構體層面提升到了mod範圍,結構體本身沒有類似的概念,這一點相比 c++,java,golang 均不同,此處的修改省去了friend 友元, gettter, setter等等一些列不必要的麻煩, 同一個module內部的所有結構體函數等等可以緊密合作,簡單直接,對外暴露的函數和類型單獨增加pub關鍵字導出, 優雅而且編碼友好

crate 的直觀理解

rust 共享代碼模塊的公網地址為crates.io, 如果使用過mavenrpm等類似工具應該對此類地址並不陌生;

現在我們拆解一個crates.io上下載最多的rand模塊看一下其中的目錄結構,瞭解一下一個 rust crate 大致有什麼樣的目錄和文件結構

➜  rand git:(master) tree -L 1
.
├── CHANGELOG.md
├── COPYRIGHT
├── Cargo.toml       # 核心的Cargo.toml文件
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── SECURITY.md
├── benches
├── examples
├── rand_chacha
├── rand_core       # 一個單獨的子crate
├── rand_distr
├── rand_hc
├── rand_pcg
├── rustfmt.toml
├── src              # `rand`的src目錄 
└── utils
9 directories, 8 files


## lib.rs 是比較特殊的一個文件名,一個crate一般只有一個,是默認的crate的導出點和入口文件.
➜  rand git:(master) tree -L 1 src
src
├── distributions
├── lib.rs
├── prelude.rs
├── rng.rs
├── rngs
└── seq
3 directories, 3 files
## 仔細研究`rand_core`你會發現它和外部的`rand`是同構的
➜  rand git:(master) tree -L 2 rand_core
rand_core
├── CHANGELOG.md
├── COPYRIGHT
├── Cargo.toml        # `rand_core`的`Cargo.toml` crate描述文件
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
└── src
    ├── block.rs
    ├── error.rs
    ├── impls.rs
    ├── le.rs
    ├── lib.rs      # `rand_core`的`src/lib.rs`文件
    └── os.rs
1 directory, 12 files

可以看到,rand crate本身是以外部依賴其他crate(比如rand_core), 我們自己寫代碼的時候也可以按照這種方式組織代碼,雖然工程上看代碼屬於同一個目錄結構和工程內,但是實際上其中的代碼編譯關係是有明確的分割關係的. 因為crate本質上是類庫的同等地位,所以一個crate只允許一個lib (lib.rs) 編譯入口文件,但是可以允許有多個main函數(依賴核心的 lib.rs)單獨編譯可執行文件 (binary crate);

註釋和文檔

相比於 C++ 等古老的語言生態,rust 的生態支持解決了太多痛點,比如crates.io的存在保證了代碼的編寫,發佈,文檔的一體化,一切都是簡單的 cargo 命令即可完成;既然說到了文檔,就要知道 cargo 還有cargo doc命令,只要使用markdown編寫的註釋便可直接生成對應的html文檔,其開發友好程度讓人大呼過癮.

rust 的註釋寫法簡單說有如下幾種:

// 普通註釋,此類註釋cargo doc會忽略 •/// 文檔註釋 ,一般函數結構提元素的註釋寫法,內容可以是 markdown 語法,如果包含代碼段,代碼會被自動生成測試代碼 •//! 高層的文檔註釋,此類註釋生成的是高層模塊描述和簡介

#![allow(unused)]
fn main() {
cargo new --lib myutil
修改src/lib.rs為如下內容
//! # The first line
//! The second line
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let five = 5;
///
/// assert_eq!(6, add_one(5));
/// # fn add_one(x: i32) -> i32 {
/// #     x + 1
/// # }
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}
}

以上面代碼為例,生成文檔的過程如下:

cargo doc --no-deps
cargo doc --open

得到的自動化文檔如下:

img

學 Rust 要有大局觀 -二- Rust 的精髓

上一篇 (學 Rust 要有大局觀) 我們從 rust 的安裝部署,到 cargo 的基本使用,給大家做了科普,為了保證可以降低 Rust 的學習難度,一開始我們必須掃除掉除了基本語法之外的核心難點,這一篇我們關注於所有權(+ 生命週期) 這個 Rust 最難學的部分, 但是樑小孩今天十分鐘之內爭取讓你有學習 Rust 的戰略思維,知道 rust 應該怎麼學~

Rust 精髓

我嘗試用幾個簡單的詞彙說明 Rust 的設計精髓和底層原理,方便對比其他語言和 Rust 的不同之處

•Rust 變量具有閱後即焚的特性, 相比之下其他語言的變量都是耐用品, 而 Rust 的變量屬於一次性用品•Rust 語言中變量使用值擁有是明確區分的,而且其他語言的變量等號基本都是賦值,但是 Rust 是所有權讓渡

三種常見的內存模式

從下面三行簡單的賦值語句, 我們直觀感受一下 c++,python, 還有 Rust 的不同處理方式

# c++代碼,僅僅用來說明簡單邏輯
auto s = std::vector<std::string>{ "udon", "ramen", "soba" }; 
auto t = s;    // 第一次使用s
auto u = s;    // 第二次使用s

c++ will copy

img

c++-copy

棧內的變量一直增長 (從左往右),變量也一直可以被訪問使用. 而且棧到堆的指向關係互相交織(網狀); 由於 C++ 默認採用 copy 的方式進行operator=的操作,即使是std::vector複雜的 STL 結構,都是直接複製, 鑑於這種默認動作開銷比較大,一般程序員會手工引入引用或者指針來優化 (問題隨之而來,棧到堆的指針遲早變成網)

Python will count

img

python3-count

對於 python 而言,由於有 gc 的存在,gc 採用了reference count技術,所以邏輯層次上多了一個 PyObject 的中間層,保存了計數信息, 由於採用的是計數機制,而 python 棧上的變量都是對同一個值的多個引用,修改其中一個總是會讓其他變量的值也都一起變化

Rust will move (and crush!!)

img

rust-will-move-and-crush

對於 Rust 而言,變量的賦值操作等同於值擁有權的讓渡, 它的意義就是auto t = s;這種語句一旦執行,相當於棧變量t取代了棧變量s,擁有了底層的值, 隨之而來的就是s在編譯階段就被編譯器標識為不再可用; 這是 rust 編譯器處理代碼的邏輯,所以auto u = s;這樣的語句根本不會通過編譯,更無需再考慮代碼執行; x 這一切都發生在代碼分析階段,編譯過程中, 不管是簡單的賦值,還是被函數形參,還是一個值被從函數返回: 都是直接的管理權讓渡

思考題

一個 for 循環,內部一個 print 函數打印了上面列表,這樣一個簡單的邏輯不同的語言會出現什麼樣的內存結果?

•c++ OK, s 可以再次被賦值使用,打印,進行各種操作 •Python OK, s 也可以正常使用 •Rust OK, 可以正常打印,但是 for 訓話結束之後,所有字符串堆中的資源都被釋放了, s 變成了不可再用的變量

好了,是不是感覺太神奇了~~, Rust 對變量的使用就是直接拿來,如果沒有新的上下文接受讓渡, 變量就被直接銷燬了, 這個神奇的設定就是 Rust 有別於其他各種語言,並且會有move sematicborrow, lifetime的最底層設定, 這就是 rust 的遊戲規則.

簡單發散思考, 這個神奇的move sematic設定會導致什麼樣的直觀現象呢,類似與 c++,java,python 各種語言其實隨著程序一點一點執行,可能會有成百上千的 object 產生,其中的變量指針,引用,copy 互相交織在一起,看起來就會亂糟糟的,這常被稱之為對象之海(如下圖)

img

a sea of objects

由於 rust 特立獨行的底層遊戲規則,不管程序運行了多久,邏輯上看來,不管對象內部有多少子元素,列表還是字典,永遠只有一個 root(擁有它), 再加上我們將要說到的使用權的限制,Rust 的堆棧變量總是非常乾淨清楚 (給你了,你就是owner), 你不會有類似 c++ 中三方庫函數返回了一個指針, 我應該free?的疑問.(如下圖)

img

rust object tree

本文簡答說明瞭 Rust 最核心的底層設計,關於所有權的相關內容,我們可以詳細展開了, 敬請期待

學 Rust 要有大局觀 -三- 最痛就這麼痛了

導語

讀過上一篇 (學 Rust 要有大局觀 (二) Rust 的精髓) 的同學直接給我反饋的問題主要是: 為什麼 Rust 要有move sematic這個神奇設定,你說的都懂,但是好處在哪裡呢?(歡迎大家有問題直接在公眾號 "Rust 工程實踐" 留言提問, 讀者的反饋真的是寫作的原始反饋和動力). 所以在開始今天的reference & lifetime主題之前,我們簡單回顧一下轉移語義到底的好壞之處有哪些:

move sematic 的好壞

好處(rust 的承諾)

  1. 方便編譯器編譯階段跟蹤內存值的使用情況,可以讓編譯器無比強大地分析堆棧狀態.2. 編譯階段排除了非常多內存不安全的內存 (代碼寫法), 不會存在懸空指針 (dangling pointer)

壞處

• 學習曲線陡峭的重要原因之一,會進一步引出引用,生命週期等概念,容易讓初學者遭遇挫敗感 • 連一個賦值都 TM 編譯不過!?• 為什麼一個 print 打印之後,變量就沒了!?

rust 痛點排行

根據Rust Survey 2020 Results[1] 調查結果顯示,rust 最難學的部分以lifetime排第一位. 全局觀很重要,今天我們就開始帶大家看一看最難的部分到底有多難,最痛也就這麼痛了. 可以看到生存週期,所有權,還有 trait,是大家掌握起來比較棘手主題.

img

rating-of-topics-difficulties

rust reference

上一篇的末尾我們提到過一個簡單的思考,基於move sematic編寫代碼的時候,一個簡單的 for 循環 print 語句就會導致一個數組變量被使用後釋放掉,這其中的核心原因就是 for 循環語句理論上應該是租借使用權, 而不應該取得所有權; 為瞭解決這個問題,rust 提供了reference類型的可copy變量;

作為一個 c/c++ 程序員的你, 請思考,把上面 for 循環和打印語句作為函數體的情況下 (僅有外部變量的讀取需求),如果在其他語言中,如此簡單的函數有可能造成程序崩潰嗎?或者會有什麼陷阱?

你的答案可能是這樣的; 函數的參數應該是const &, 常量引用,這樣我即避免了外部變量的拷貝,節省了內存和調用開銷,同時通過 const 保證了不改變使用的變量內容.

我想說的是,這都是沒問題的,但是這樣並不能保證你的完美print函數 core 掉,原因是,你通過const &對編譯器承諾了自己不改變變量,僅僅是使用,也不做 copy,然而gcc編譯器並不給你任何承諾,所以:

  1. 變量其他地方被改掉了. 你讀取到了奇怪的內容.2. 變量被銷燬了,你讀取到了不應該操作的內存,程序崩掉了.

為什麼可以這樣?因為程序員承諾我不修改我使用的內容,但 c++ 語言本身,編譯器並不承諾這個變量它自己不會變(這種承諾不是相互的);

在 Rust 中, 程序員通過borrow得到一個reference來承諾僅讀取,或者肯定會修改一個變量

borrow的承諾寫法是在變量前增加一個&, 比如

#![allow(unused)]
fn main() {
struct Point {
    x: i32,
    y: i32 
}
let point = Point { x: 1000, y: 729 };
let r: &Point = &point;     // r現在是point的`只讀使用權`
let rr: &&Point = &r;        // rr現在是`只讀使用權`的`只讀使用權`
let rrr: &&&Point = &rr;    // rr現在是`只讀使用權`的`只讀使用權`的`只讀使用權`
}

上面的代碼的內存模型是這樣的:

A-chain-of-references-to-references

so~ 很明顯,如果使用r來訪問point的話,並不會影響 point 的所有權關係,即使r被閱後即焚了也不影響point, 內存上它倆使用的棧資源沒關係.

於此同時,編譯器同時許諾你在你使用這個 reference 期間, 任何對原有變量所有權的變動的代碼,及修改,我都拒絕編譯! 編譯器通過如下圖所示的規則來判斷是否拒絕代碼的編譯, 詳細來說,rustc 通過兩條規則來兌現它的承諾 (下圖的中間和最右則情況):

reference-and-ownership

詳細來說就是,編譯器做代碼靜態分析的時候,僅通過閱讀文本符號就知道是否要編譯此代碼還是直接拒絕繼續編譯代碼,因為borrow語意的語法是精確的,就是程序員給出的承諾. 以一個簡單的結構體來說,內部還有一個其他結構體,那麼它的內存模型大致就是上圖最左邊的樣子 (棧上是變量本身的空間,內容是指向堆內的資源的樹形結構). 下面以程序員要操作結構體內部的結構體子元素為例:

  1. 當程序員承諾對一個子成員變量僅讀取的時候 (中圖),編譯器承諾的兌現動作就是 • 對象樹 root 所有權變動的代碼編譯不可以通過 • 修改子成員內容的代碼編譯不可以通過 • 值 owner 也最多做讀取不能再多了 2. 當程序員承諾對一個子成員變量要修改的時候 (右圖),編譯器承諾的兌現動作就是 •非承諾的引用`之外的所有試圖接觸這個值的代碼都不可以通過 • 即使是通過值的 owner 也不行讀取

基於以上兩條規則,請自己編寫 rust 的編譯器,給出下面代碼是否違反給程序員的承諾?(注意同樣的代碼寫法,若是 c++ 肯定是可以通過編譯的)

fn main() {
    let mut v = 10;    // v是一個可以改變的變量
    let r = &v;    // `&v` 就是程序員的承諾: 用r(reference)來借用變量v的使用權(讀取)
    let rr = &r;    // 程序員二次承諾還要用rr也讀取v, 並且
    v += 10        // 要改變v了,編譯過還是不過? 
    myprint(r);    // 打印函數, 使用r來訪問 
    myprint(rr);    // 打印函數, 使用rr來訪問
}

你是否疑惑上面的文字裡,樑小孩反覆地寫程序員得到一個引用是承諾對一個變量做只讀操作? 我就是讀取一下,有什麼要承諾的?

現在我要告訴你,rust 的語法規則之精確,讓你寫任何一句話都是在給編譯器訴說自己的承諾. 比如當你寫一個任意函數的時候,函數的聲明形式就是你給編譯器的承諾, 承諾對變量是如何使用的, 當你承諾只讀,但是函數體內部出現了寫操作,編譯器有權根據你先前的承諾拒絕編譯你的代碼.

再思考另外一種情況,多個變量的函數,一個只讀承諾,一個寫引用的承諾,還有一個是直接 move 語意的所有權取得;這就是你給編譯器的承諾,變量會被函數形參拿去值的所有權,如果使用不當被銷燬,是程序員自己一開始就不應該做出的此函數對所有權負責的承諾. 所以程序員一定要編碼之前想好頂層架構,因為一旦有變動,可能很多函數需要重寫.

生命週期

為了適應move sematic而引入的borrow & reference會帶來的新的問題是, 讓所有權和使用權發生了切割分離之後,引用和原始值的內存空間獨立,但是二者的邏輯關係要求: 原始值必須存在的前提下,引用才有存在的意義. 如果遇到返回值引用了的函數這樣的代碼是否要停止編譯? 很明顯函數返回了使用權, 如果此時不發生move需要有其他維度幫忙判斷代碼的邏輯合理性. 答案就是需要考慮lifetime生命週期.

從最原始的疑問開始: 你編寫了一個函數,並且返回了一個reference, 假如被引用的值是函數內部 local 變量,那麼基於安全考慮我們要拒絕它,如果引用的傳入參數的某個子元素,那麼我們要知道傳入參數的存活時間能否支撐這個引用是有效的. 所以編譯器一定跟蹤引用和對應變量是否存在衝突的讀寫情況,還要跟蹤每個變量的有效範圍,發現程序員讀寫違反承諾, 或者引用的生命週期不是被引用值生命週期的子集的時候,合情合理地拒絕編譯.

img

reference-with-a-lifetime

說了這麼多,其實只有圖中表示的一個核心原理,那就是 rust 編譯代碼要檢查是否存在像這樣的合理的嵌套 (cover) 關係.

如果事情到此為止的話一切完美,不過有很多中情況作用域的嵌套比上面的例子要更加隱晦, 比如一個結構體的成員borrow的外部資源, 比如一個函數調用其實就是變量進入新的作用域, 這種情況可能還會產生組合: 你得到一個內部包含了reference的結構體作為函數參數, 這時候資源跟蹤情況就很複雜了。此時,rust 編譯器要求你給出明確的關於資源存活時間的承諾. 這種承諾的表現方式就是讓很多同學看不懂的生命週期語法. 我們不關心語法,僅僅是回到原始問題上來,不管什麼樣的語法,我們應該通過這個語法給編譯器傳遞什麼信息呢? 程序員做的任何承諾,rust 編譯器都會仔細檢查,針對這種情況,程序要要做出的承諾無非就是類似我絕對不會胡亂引用這樣的信息,比如我不會引用比結構體本身存活時間還短的變量.

宣稱使用範圍

生命週期不是作用域, 是變量被使用的那段時間, 一個結構體有兩個引用類型的成員, 其中一個引用失效時,只要可以保證它也永遠不再被用到,那也是 OK 的. 所以我建議大家將生命週期理解成為程序員宣稱的引用的合理使用範圍;

#![allow(unused)]
fn main() {
struct S {
    r32: &i32;
    r64: &i64;
}
let s;
}

給s.r32承諾範圍1

1.s.r32存活時間 (reference) 一定要小於等於被引用值的存活時間(所有人都是必死的)2.s.r32存活時間和s一樣 (蘇格拉底是人)3. 所以s的存活時間必須小於等於s.r32引用的值的存活時間 (蘇格拉底是必死的)

給s.r64承諾範圍2

1.s.r64存活時間 (reference) 一定要小於等於被引用值的存活時間(所有人都是必死的)2.s.r64存活時間和s一樣 (蘇格拉底是人)3. 所以s的存活時間必須小於等於s.r64引用的值的存活時間 (蘇格拉底是必死的)

三段論裡蘇格拉底是人這個特殊陳述應該是問題的核心,因為當有多個成員變量的時候,被引用的具體值的生命週期可能一樣, r32 和 r64 原始值作用域會不同,但是無論如何s都是兩者之中更小的那一個.

編譯器處理上面的代碼的時候需要程序員承諾: 到底s.r32s.64的範圍一樣還是不一樣,你若是宣稱一樣,那麼我檢查s的存在多久就可以了,你若是宣稱不一樣, 那麼我就得按照相對小的那個來判斷s的使用範圍是否合理.

OK, 是時候看一下實際代碼了~

#![allow(unused)]
fn main() {
struct A<'a> {
    r32: &'a i32;
    r64: &'a i64;
}
struct S<'a, 'b> {
    r32: &'a i32;
    r64: &'b i64;
}
}

這就是添加了lifetime聲明的結構體,其中A宣稱A.r32A.r64預期使用範圍一樣, 此時 rustc 編譯器按照 A 的實例存活時間判斷就可以了跟蹤實際引用的值是否滿足要求, 但是S現在宣稱S.r32S.r64是兩個不同的使用範圍,此時 rustc 將分別跟蹤被引用的兩個值的存活時間是否都比S要大.

重新回到返回引用的函數這個原始問題上, 怎麼寫才合適?

// 程序員宣稱函數使用的時候, 返回值使用範圍和入參肯定一樣(或更小), rust會檢查確認是否真的這樣
fn smallest<'a>(v1: &'a [i32], v2:&'a [i32]) -> &'a i32 { ... }

到這裡你應該明白了,lifetime真的是一個編譯期概念,是程序員做出的承諾,rustc 會根據你的承諾檢查代碼是否是你宣稱的那樣,被引用的值是不是一直比引用時間更久.

到此為止,我們應該可以更清楚地理解一下move sematicborrow & reference再到liftime的整個邏輯鏈條。他們到底都在解決什麼問題, 這正學習的時候需要大量的例子加深細節把握. 我們僅關心概念和概念提出的場景,解決的問題.

以上是我自己對這些概念的理解和思考,難免會有重大錯誤,但是應該能幫到大家. 下一篇咱們看trait是個什麼東西, 再會~~

引用鏈接

[1] Rust Survey 2020 Results: https://blog.rust-lang.org/2020/12/16/rust-survey-2020.html

學 Rust 要有大局觀 -四- rust trait 的概念認知

導語

通過前三篇 (學 Rust 要有大局觀 (三)) 我們分別看了move sematic, borrow & reference以及liftime三個核心概念; 之前其實並沒有深入到任何 rust 的語法細節上,或者說盡量規避語法的講解,來到第四篇, 樑小孩仍然希望主要講解核心概念為主,代碼儘量用偽碼, 避免大家進入語法的細枝末節. 今天我們關注traits

traits 是什麼

如果你沒有其他語言中 traits 的認知,那麼你將無法瞬間明白特性這個中文翻譯背後的實際意義,通常大家會說 traits 是 interface,而實際上 interface 和 traits 的關係更像是 “錘子必須可以砸釘子” 和“給錘子添加砸核桃功能”,理解這兩二者的區別之處在於,是否允許 “錘子” 製造的時候就知道它將來可以被用來砸核桃,這個時間差就是理解 traits 和 interface 微妙差異的精髓所在,錘子製造商和錘子使用者通常不是同一個人;換言之:traits 可以作用在一個預定義的類型上,給它添加某些特性, 並且無需修改該類型本身的所有聲明和原始定義的代碼,這樣不接觸原始類型定義和聲明代碼就可以給一個類型添加功能 (function) 的特性 (traits) 使得你無須將 “錘子” 重新封裝成為 “砸核桃工具” 這樣的新類型,沒有了這層封裝,代碼上寫法將靈活許多,第一次體會到這一點的程序員肯定會感覺非常震撼,

本篇的整體精髓大局觀,其實就是上面一段話而已,下面所有內容都是細節,下面讓我們一起看一下 traits 的實際實現細節.

traits 的內存表示

通過如下代碼我們可以得到一個特型目標,它在內存中實際上就是一個胖指針.

#![allow(unused)]
fn main() {
use std::io::Write;

let mut buf: Vec<u8> = vec![];
let writer: &mut Write = &mut buf;
}

img

vtable

看明白上面圖的內存表示的話,如果你是 C++ 程序員應該就可以說基本上接近掌握了 traits 的精髓了。相比於 c++ 將 vptr 和對象成員保存在一起,rust 中類型數據就是數據本身,單獨實現的 traits 方法則是單獨聚合為一個胖指針這其中的好處非常微妙;明白了這個機制你也就可以知道為什麼對於標準類型 int 和 string 你還可以給它們擴展自己的方法; 而這是 c++ 做不到的。

同時,上面的內存模型是編譯期確定的,這也是 traits 為什麼是一種static dispatch;

語言內置 traits

rust 語言默認提供了 traits 的常見實現,編譯器可以自動處理已有類型擴展這些 traits;它們分別是:

•Eq, PartialEq, Ord, PartialOrd•Clone, 用來從 &T 創建副本 T。•Copy,使類型具有 “複製語義”(copy semantics)而非 “移動語義”(move semantics)。•Hash,從 &T 計算哈希值(hash)。•Default, 創建數據類型的一個空實例。•Debug,使用 {:?} formatter 來格式化一個值。

內置 traits 用起來就像特定的註解

閱讀代碼吧,並且體會它與 java 註解的使用體驗,或許你會馬上理解其精髓。

#![allow(unused)]
fn main() {
//  CppStruct變量綁定、函數參數傳遞、函數返回值傳遞時將被複制, 而不是borrow

// 1. 編譯器支持的Copy traits
 #[derive(Copy, Debug)]
 pub struct CppStruct {
     pub x: i32,
     pub y: i32,
 }

impl Clone for CppStruct {
    fn clone(&self) -> Self {
        Self {
            x: self.x + 1,
            y: self.y + 1,
        }
    }
}

// 2. 用法及生效情況, 注意x1和x2的區別
let x0 = CppStruct { x: 0, y: 1 };
let x1 = x0;
let x2 = x0.clone();
println!("x1.value={:?}, x1.address={:p}", x1, &x1);
println!("x2.value={:?}, x2.address={:p}", x2, &x2);
println!("reuse variable x0={:?}, x0.address={:p}", x0, &x0);

// 輸出結果
x1.value=CppStruct { x: 0, y: 1 }, x1.address=0x16d24f0e8
x2.value=CppStruct { x: 1, y: 2 }, x2.address=0x16d24f0f0
reuse variable x0=CppStruct { x: 0, y: 1 }, x0.address=0x16d24f0e0
}

編碼自己的 traits

自定義 traits 時,它更像是一種接口聲明

#![allow(unused)]
fn main() {
/// STEP1:   抽象一個機器學習Optimizer應該具有的核心功能,定義為一個trait
pub trait Optimizer<'a> {
    // 有默認實現的函數build_params
    fn build_params<U, T: From<U>>(params: Vec<U>) -> Vec<T> {
        let mut vec = Vec::with_capacity(params.len());
        for param in params {
            vec.push(T::from(param));
        }
        vec
    }
    // 無默認實現的函數,get_lr
    fn get_lr(&self) -> f32;

    // 無默認實現的函數,set_lr
    fn set_lr(&self, lr: f32);
}

/// STEP2: 實現一個SGD優化邏輯,並重載核心函數
pub struct SGD<'a, T> {
    params: RefCell<Vec<SGDParam<'a>>>,
    lr: Cell<f32>,
    penalty: T,
}

impl<'a, T: Penalty> Optimizer<'a> for SGD<'a, T> {

    fn get_lr(&self) -> f32 {
        self.lr.get()
    }

    fn set_lr(&self, lr: f32) {
        self.lr.set(lr)
    }
}
}

將 trait 理解為接口是一種很符合直覺的簡化,但是你只要知道它不等同於接口,只是可以用起來很像接口

結語(2022/02/13 03:34 成文)

這次關於 trait 最核心的內容就總結到此,實際使用時能明白其中的精髓就好了,關於 trait 的實際功能其實比本次介紹的要更多,但是筆者認為有大局觀,知道它是什麼,對它有清晰的概念認知的話,上面的內容已經足夠。收集更多問題後,後續找更多的例子來詳細展開用法也不遲。

學 Rust 要有大局觀 -五- 屬性的功能

什麼是 attribute?

attribute 是 rust 編譯器留給程序員的交互接口,一段代碼可以編譯產出為二進制機器碼的過程通常來說,用戶的代碼是編譯器的輸入,當編譯器認為代碼有問題,而程序員認為沒問題的時候,必須允許程序員和編譯器有交互,允許程序員指導個別代碼的處理方式。

下面列舉幾個比較有代表性的場景:

讓編譯器閉嘴

rustc 要求結構體的命名需為駱駝體,否則就會給出警告,假如程序員‘故意’要給出一個結構體,但是用了 python 的下劃線命名,為了讓編譯器忽略對命名格式的‘異議’,可以通過

#![allow(unused)]
fn main() {
#[allow(non_camlel_case_types)]
}

這個標記,把它放在結構體聲明之前,編譯器就可以按照程序員的意願主動放過。而不是輸出一堆警告。

讓編譯器臨場應變

所謂的臨場應變經典場景主要是硬件和操作系統環境區別,比如當前這個程序員的操作系統是windows, CPU 架構是x86_64又或者是arm64, 代碼編譯的時候有些別人的代碼確實不可能編譯通過,程序員需要主動處理條件編譯規則,指導編譯器不同架構下同一個代碼應該編譯哪一個具體實現。對應的 attribute 類似#[cfg(windows)]的標記說明具體信息。

讓編譯器改變默認行為

rust 瞭解之後你會知道有很多特有的術語,比如‘氧化’表示用 rust 去重寫一些現有的庫,或者用 rust 去實現一個特定的功能,但是畢竟很多東西編譯之後會有一些歷史命名習慣或者接口規則,不能按照 rust 本身的約定輸出,我們可以通過

#![allow(unused)]
#![crate_type = "cdylib"]
#![create_name=''crypto3]
fn main() {
}

這樣我們得到的名字是libcrypto3.so,我們還特意多加了一個3o後面,表示這個庫是我們自己用 rust 氧化之後的版本; 否則我們得到的是一個原版命名的libcrate.rlib這樣的 rust 二進制庫名字.

讓編譯器區別對待一些函數和代碼

單測就是這樣一個典型場景,相比於其他語言用文件名後綴,前綴,或者是引入某些包單獨編碼而言,rust 支持的更為簡單直接,只要有 #[test]修飾的函數都會標記為測試函數,會在cargo test命令下被拉出來單獨執行。

讓編譯自動添加一些行為

這個用法就非常多樣了,典型場景是用#[derive(Debug)] 類似的標記好,以便編譯器自動給我們的類型添加特性.

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Point {
    x:f32,
    y:f32,
}
}

編譯器現在會給 Point 類自動添加一個fmt函數,這個函數功能類似 java 的toString, 或者 python 的__repr__等,注意:derive 的意義為派生,但這裡並沒有一個默認的父類實現, 代碼都是編譯器根據當前的類程成員變量自動添加並編譯的。

derivetrait搭配使用共同左右,想要明白其中細節可以閱讀本系列上關於trait的單獨篇章.

結語

看了一些快速簡單的例子,你應該知道 attribute 這個概念是什麼內涵了,知道了內涵也就明白了概念,遇到的時候就不會有疑惑了。下一篇我們講一下 rust 惱人的宏,用不用不重要,重要的時候通過瞭解 rust,大家一起思考一下手頭使用的語言到底哪裡不好,為什麼有新的設計。

理解 Rust 字符串

Rust 中有多種表示字符串的數據類型,其中最常用的是 str 和 String 兩種類型。

str 類型

Rust 中有一個表示字符串的原始(primitive)類型 str。str 是字符串切片(slice),每個字符串切片具有固定的大小並且是不可變的。通常不能直接訪問 str ,因為切片屬於動態大小類型(DST)。所以,只能通過引用(&str)間接訪問字符串切片。關於這一點,會在以後的文章中介紹。在下面的內容將不加區分的使用 str 和 &str。

可以通過字符串字面量構造 &str 類型的對象:

#![allow(unused)]
fn main() {
let s = "Hello, world!";
}

在 Rust 中,字符串字面量的類型是 & 'static str,因為它是被直接儲存在編譯後的二進制文件中的。

還可以使用切片的語法,從一個&str 對象構造出另一個 &str 對象:

#![allow(unused)]
fn main() {
let ss = &s[..3];
}

也可以將切片轉換成相應的指針類型:

#![allow(unused)]
fn main() {
let p = s as *const str;
}

String 類型

像大部分常見的編程語言一樣,String 是一個分配在堆上的可增長的字符串類型,它的定義如下:

#![allow(unused)]
fn main() {
struct String { 
    vec: Vec<u8> 
}
}

從源碼可以看出,String 是對 Vec 的簡單包裝。

String 保存的總是有效的 UTF-8 編碼的字節序列。

構造一個空字符串:

#![allow(unused)]
fn main() {
let s = String::new();
}

還可以通過字符串字面量構造 String 類型的對象:

#![allow(unused)]
fn main() {
let hello = String::from("Hello, world!");
}

String 和 &str 之間有著非常緊密的關系,後者可以用來表示前者的被借用(Borrowed)的副本。

str 和 String 類型的轉換

前面已經看到,字符串字面量可以轉換成 String。反過來,String 也可以轉換成str。這是通過解引用操作實現的:

#![allow(unused)]
fn main() {
impl Deref for String { 
    fn deref(&self) -> &str { 
        unsafe { str::from_utf8_unchecked(&self.vec) } 
    } 
} 
}

利用解引用操作就可以將 String 轉換成 str:

#![allow(unused)]
fn main() {
let s: String = String::from("Hello");
let ss: &str = &s;
}

String 還可以連接一個 str 字符串:

#![allow(unused)]
fn main() {
let s = String::from("Hello"); 
let b = ", world!"; 
let f = s + b; // f == "Hello, world!"
}

如果要連接兩個 String 對象,不能簡單地直接相加。必須先通過解引用將後一個對象轉換為 &str 才能進行連接:

#![allow(unused)]
fn main() {
let s = String::from("Hello");
let b = String::from(", world!");
let f = s + &b; // f == "Hello, world!"
}

注意這裡字符串連接之後,s的所有權發生了轉移,而b的內容復制到了新的字符串中。

從 String 到 str 的轉換是廉價的,反之,從 str 轉為 String 需要分配新的內存。

一般來說,當定義函數的參數時, &str 會比 String 更加通用:因為此時既可以傳遞 &str 對象也可以傳遞 String 對象。

更新

example workflow name

2021年2月1日: Youtube視頻

2021年1月4日: 支持在線查看 點擊閱讀

介紹

Rust是一種新的語言,已經有了很好的教科書。但是有時候它的教材很難,因為它的教材是給以英語為母語的人看的。現在很多公司和人學習Rust,如果有一本英語簡單的書,他們可以學得更快。這本教材就是給這些公司和人用簡單的英語來學習Rust的。

Rust是一門很新的語言,但已經非常流行。它之所以受歡迎,是因為它給你提供了C或C++的速度和控制力,但也給你提供了Python等其他較新語言的內存安全。它用一些新的想法來實現這一點,這些想法有時與其他語言不同。這意味著有一些新的東西需要學習,你不能只是 "邊走邊想"。Rust是一門語言,你必須思考一段時間才能理解。但如果你懂其他語言的話,它看起來還是很熟悉的,它是為了幫助你寫好代碼而生的。

我是誰?

我是一個生活在韓國的加拿大人,我在寫Easy Rust的同時,也在思考如何讓這裡的公司開始使用它。我希望其他不以英語為第一語言的國家也能使用它。

簡單英語學Rust

簡單英語學Rust寫於2020年7月至8月,長達400多頁。如果你有任何問題,可以在這裡或在LinkedIn上在Twitter上聯繫我。如果你發現有什麼不對的地方,或者要提出pull request,請繼續。已經有超過20人幫助我們修復了代碼中的錯別字和問題,所以你也可以。我不是世界上最好的Rust專家,所以我總是喜歡聽到新的想法,或者看看哪裡可以讓這本書變得更好。

第1部分 - 瀏覽器中的Rust

本書有兩個部分。第1部分,你將在瀏覽器中就能學到儘可能多的Rust知識。實際上你幾乎可以在不安裝Rust的情況下學到所有你需要知道的東西,所以第1部分非常長。最後是第二部分。它要短得多,是關於電腦上的Rust。在這裡,你將學習到其他一切你需要知道的、只能在瀏覽器之外進行的事情。例如:處理文件、接受用戶輸入、圖形和個人設置。希望在第一部分結束時,你會喜歡Rust,以至於你會安裝它。如果你不喜歡,也沒問題--第一部分教了你很多,你不會介意的。

Rust Playground

也許你還不想安裝Rust,這也沒關係。你可以去https://play.rust-lang.org/,在不離開瀏覽器的情況下開始寫Rust。你可以在那裡寫下你的代碼,然後點擊Run來查看結果。你可以在瀏覽器的Playground裡面運行本書中的大部分示例。只有在接近結尾的時候,你才會看到無法在Playground運行的示例(比如打開文件)。

以下是使用Rust Playground時的一些提示。

  • 用"Run"來運行你的代碼

  • 如果你想讓你的代碼更快,就把Debug改為Release。Debug:編譯速度更快,運行速度更慢,包含調試信息。Release:編譯速度更慢,運行速度更快,刪除調試信息。

  • 點擊Share,得到一個網址鏈接,你可以用它來分享你的代碼。如果你需要幫助,可以用它來分享你的代碼。點擊分享後,你可以點擊Open a new thread in the Rust user forum,馬上向那裡的人尋求幫助。

  • Rustfmt工具: Rustfmt會很好地格式化你的代碼。

  • TOOLS: Rustfmt會很好地格式化你的代碼。Clippy會給你額外的信息,告訴你如何讓你的代碼更好。

  • CONFIG: 在這裡你可以把你的主題改成黑暗模式,這樣你就可以在晚上工作了,還有很多其他配置。

如果你想安裝Rust,請到這裡https://www.rust-lang.org/tools/install,然後按照說明操作。通常你會使用rustup來安裝和更新Rust。

🚧和⚠️

有時書中的代碼例子不能用。如果一個例子不工作,它將會有一個🚧或⚠️在裡面。🚧就像 "正在建設中"一樣:它意味著代碼不完整。Rust需要一個fn main()(一個主函數)來運行,但有時我們只是想看一些小的代碼,所以它不會有fn main()。這些例子是正確的,但需要一個fn main()讓你運行。而有些代碼示例向你展示了一個問題,我們將解決這個問題。那些可能有一個fn main(),但會產生一個錯誤,所以它們會有一個⚠️。

註釋

註釋是給程序員看的,而不是給電腦看的。寫註釋是為了幫助別人理解你的代碼。 這也有利於幫助你以後理解你的代碼。 (很多人寫了很好的代碼,但後來卻忘記了他們為什麼要寫它。)在Rust中寫註釋,你通常使用 //

fn main() {
    // Rust programs start with fn main()
    // You put the code inside a block. It starts with { and ends with }
    let some_number = 100; // We can write as much as we want here and the compiler won't look at it
}

當你這樣做時,編譯器不會看//右邊的任何東西。

還有一種註釋,你用/*開始寫,*/結束寫。這個寫在你的代碼中間很有用。

fn main() {
    let some_number/*: i16*/ = 100;
}

對編譯器來說,let some_number/*: i16*/ = 100;看起來像let some_number = 100;

/* */形式對於超過一行的非常長的註釋也很有用。在這個例子中,你可以看到你需要為每一行寫//。但是如果您輸入 /*,它不會停止,直到您用 */ 完成它。

fn main() {
    let some_number = 100; /* Let me tell you
    a little about this number.
    It's 100, which is my favourite number.
    It's called some_number but actually I think that... */

    let some_number = 100; // Let me tell you
    // a little about this number.
    // It's 100, which is my favourite number.
    // It's called some_number but actually I think that...
}

類型

Rust有很多類型,讓你可以處理數字、字符等。有些類型很簡單,有些類型比較複雜,你甚至可以創建自己的類型。

原始類型

Rust有簡單的類型,這些類型被稱為原始類型(原始=非常基本)。我們將從整數和char(字符)開始。整數是沒有小數點的整數。整數有兩種類型。

  • 有符號的整數
  • 無符號整數

符號是指+(加號)和-(減號),所以有符號的整數可以是正數,也可以是負數(如+8,-8)。但無符號整數只能是正數,因為它們沒有符號。

有符號的整數是 i8, i16, i32, i64, i128, 和 isize

無符號的整數是 u8, u16, u32, u64, u128, 和 usize

i或u後面的數字表示該數字的位數,所以位數多的數字可以大一些。8位=一個字節,所以i8是一個字節,i64是8個字節,以此類推。尺寸較大的數字類型可以容納更大的數字。例如,u8最多可以容納255,但u16最多可以容納65535。而u128最多可以容納340282366920938463463374607431768211455。

那麼什麼是isizeusize呢?這表示你電腦的位數。(你的電腦上的位數叫做你電腦的架構)。所以32位計算機上的isizeusize就像i32u32,64位計算機上的isizeusize就像i64u64

整數類型不同的原因有很多。其中一個原因是計算機性能:較小的字節數處理速度更快。例如,數字-10作為i811110110,但作為i12811111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110110。但這裡還有一些其他用法。

Rust中的字符叫做char. 每一個char都有一個數字:字母A是數字65,而字符(中文的 "朋友")是數字21451。這個數字列表被稱為 "Unicode"。Unicode對使用較多的字符使用較小的數字,如A到Z,或0到9的數字,或空格。

fn main() {
    let first_letter = 'A';
    let space = ' '; // A space inside ' ' is also a char
    let other_language_char = 'Ꮔ'; // Thanks to Unicode, other languages like Cherokee display just fine too
    let cat_face = '😺'; // Emojis are chars too
}

使用最多的字符的數字小於256,它們可以裝進u8。記住,u8是0加上255以內的所有數字,總共256個。這意味著 Rust 可以使用 asu8 安全地 castchar。("把 u8 cast成 char"意味著 "把 u8 假裝成 char")

as cast是有用的,因為 Rust 是非常嚴格的。它總是需要知道類型。 而不會讓你同時使用兩種不同的類型,即使它們都是整數。例如,這將無法工作:

fn main() { // main() is where Rust programs start to run. Code goes inside {} (curly brackets)

    let my_number = 100; // We didn't write a type of integer,
                         // so Rust chooses i32. Rust always
                         // chooses i32 for integers if you don't
                         // tell it to use a different type

    println!("{}", my_number as char); // ⚠️
}

原因是這樣的:

error[E0604]: only `u8` can be cast as `char`, not `i32`
 --> src\main.rs:3:20
  |
3 |     println!("{}", my_number as char);
  |                    ^^^^^^^^^^^^^^^^^

幸運的是,我們可以用as輕鬆解決這個問題。我們不能把i32轉成char,但我們可以把i32轉成u8,然後把u8轉換成char。所以在一行中,我們使用 as 將 my_number 變為 u8,再將其變為 char。現在可以編譯了。

fn main() {
    let my_number = 100;
    println!("{}", my_number as u8 as char);
}

它打印的是d,因為那是100對應的char

然而,更簡單的方法是告訴 Rust my_numberu8。下面是你的做法。

fn main() {
    let my_number: u8 = 100; //  change my_number to my_number: u8
    println!("{}", my_number as char);
}

所以這就是Rust中所有不同數字類型的兩個原因。這裡還有一個原因:usize是Rust用於索引的大小。(索引的意思是 "哪項是第一","哪項是第二"等等)usize是索引的最佳大小,因為:

  • 索引不能是負數,所以它需要是一個帶u的數字
  • 它應該是大的,因為有時你需要索引很多東西,但。
  • 不可能是u64,因為32位電腦不能使用u64。

所以Rust使用了usize,這樣你的計算機就可以得到它能讀到的最大的數字進行索引。

我們再來瞭解一下char。你看到char總是一個字符,並且使用''而不是""

所有的 字符 都使用4個字節的內存,因為4個字節足以容納任何種類的字符:

  • 基本字母和符號通常需要4個字節中的1個:a b 1 2 + - = $ @
  • 其他字母,如德語的 Umlauts 或重音,需要4個字節中的2個: ä ö ü ß è é à ñ
  • 韓文、日文或中文字符需要3或4個字節: 國 안 녕

當使用字符作為字符串的一部分時,字符串被編碼以使用每個字符所需的最小內存量。

我們可以用.len()來看一下。

fn main() {
    println!("Size of a char: {}", std::mem::size_of::<char>()); // 4 bytes
    println!("Size of string containing 'a': {}", "a".len()); // .len() gives the size of the string in bytes
    println!("Size of string containing 'ß': {}", "ß".len());
    println!("Size of string containing '國': {}", "國".len());
    println!("Size of string containing '𓅱': {}", "𓅱".len());
}

這樣打印出來。

Size of a char: 4
Size of string containing 'a': 1
Size of string containing 'ß': 2
Size of string containing '國': 3
Size of string containing '𓅱': 4

可以看到,a是一個字節,德文的ß是兩個字節,日文的是三個字節,古埃及的𓅱是4個字節。

fn main() {
    let slice = "Hello!";
    println!("Slice is {} bytes.", slice.len());
    let slice2 = "안녕!"; // Korean for "hi"
    println!("Slice2 is {} bytes.", slice2.len());
}

這個打印:

Slice is 6 bytes.
Slice2 is 7 bytes.

slice的長度是6個字符,6個字節,但slice2的長度是3個字符,7個字節。

如果.len()給出的是以字節為單位的大小,那麼以字符為單位的大小呢?這些方法我們後面會學習,但你只要記住.chars().count()就可以了。.chars().count() 將你寫的東西變成字符,然後計算有多少個字符。

fn main() {
    let slice = "Hello!";
    println!("Slice is {} bytes and also {} characters.", slice.len(), slice.chars().count());
    let slice2 = "안녕!";
    println!("Slice2 is {} bytes but only {} characters.", slice2.len(), slice2.chars().count());
}

這就打印出來了。

Slice is 6 bytes and also 6 characters.
Slice2 is 7 bytes but only 3 characters.

類型推導

類型推導的意思是,如果你不告訴編譯器類型,但它可以自己決定,它就會決定。編譯器總是需要知道變量的類型,但你並不總是需要告訴它。實際上,通常你不需要告訴它。例如,對於let my_number = 8my_number將是一個i32。這是因為如果你不告訴它,編譯器會選擇i32作為整數。但是如果你說let my_number: u8 = 8,它就會把my_number變成u8,因為你告訴它u8

通常編譯器都能猜到。但有時你需要告訴它,原因有兩個。

  1. 你正在做一些非常複雜的事情,而編譯器不知道你想要的類型。
  2. 你想要一個不同的類型(例如,你想要一個i128,而不是i32)。

要指定一個類型,請在變量名後添加一個冒號。

fn main() {
    let small_number: u8 = 10;
}

對於數字,你可以在數字後面加上類型。你不需要空格--只需要在數字後面直接輸入。

fn main() {
    let small_number = 10u8; // 10u8 = 10 of type u8
}

如果你想讓數字便於閱讀,也可以加上_

fn main() {
    let small_number = 10_u8; // This is easier to read
    let big_number = 100_000_000_i32; // 100 million is easy to read with _
}

_不會改變數字。它只是為了讓你方便閱讀。而且你用多少個_都沒有關係。

fn main() {
    let number = 0________u8;
    let number2 = 1___6______2____4______i32;
    println!("{}, {}", number, number2);
}

這樣打印出的是0, 1624

浮點數

浮點數是帶有小數點的數字。5.5是一個浮點數,6是一個整數。5.0也是一個浮點數,甚至5.也是一個浮點數。

fn main() {
    let my_float = 5.; // Rust sees . and knows that it is a float
}

但類型不叫float,叫f32f64。這和整數一樣:f後面的數字顯示的是位數。如果你不寫類型,Rust會選擇f64

當然,只有同一類型的浮點數可以一起使用。所以你不能把f32加到f64上。

fn main() {
    let my_float: f64 = 5.0; // This is an f64
    let my_other_float: f32 = 8.5; // This is an f32

    let third_float = my_float + my_other_float; // ⚠️
}

當你嘗試運行這個時,Rust會說。

error[E0308]: mismatched types
 --> src\main.rs:5:34
  |
5 |     let third_float = my_float + my_other_float;
  |                                  ^^^^^^^^^^^^^^ expected `f64`, found `f32`

當你使用錯誤的類型時,編譯器會寫 "expected (type), found (type)"。它這樣讀取你的代碼。

fn main() {
    let my_float: f64 = 5.0; // The compiler sees an f64
    let my_other_float: f32 = 8.5; // The compiler sees an f32. It is a different type.
    let third_float = my_float + // You want to add my_float to something, so it must be an f64 plus another f64. Now it expects an f64...
    let third_float = my_float + my_other_float;  // ⚠️ but it found an f32. It can't add them.
}

所以,當你看到 "expected(type),found(type)"時,你必須找到為什麼編譯器預期的是不同的類型。

當然,用簡單的數字很容易解決。你可以用asf32轉成f64

fn main() {
    let my_float: f64 = 5.0;
    let my_other_float: f32 = 8.5;

    let third_float = my_float + my_other_float as f64; // my_other_float as f64 = use my_other_float like an f64
}

或者更簡單,去掉類型聲明。("聲明一個類型"="告訴Rust使用該類型")Rust會選擇可以加在一起的類型。

fn main() {
    let my_float = 5.0; // Rust will choose f64
    let my_other_float = 8.5; // Here again it will choose f64

    let third_float = my_float + my_other_float;
}

Rust編譯器很聰明,如果你需要f32,就不會選擇f64。

fn main() {
    let my_float: f32 = 5.0;
    let my_other_float = 8.5; // Usually Rust would choose f64,

    let third_float = my_float + my_other_float; // but now it knows that you need to add it to an f32. So it chooses f32 for my_other_float too
}

打印hello, world!

當你啟動一個新的Rust程序時,它總是有這樣的代碼。

fn main() {
    println!("Hello, world!");
}
  • fn的意思是函數。

  • main是啟動程序的函數。

  • ()表示我們沒有給函數任何變量來啟動。

{}被稱為代碼塊。這是代碼所在的空間。

println!是一個,打印到控制檯。一個就像一個函數,為你寫代碼。宏後面有一個!。我們以後會學習如何創建宏。現在,請記住,!表示它是一個宏。

為了學習;,我們將創建另一個函數。首先,在main中,我們將打印一個數字8。

fn main() {
    println!("Hello, world number {}!", 8);
}

println!中的{}的意思是 "把變量放在這裡面"。這樣就會打印出Hello, world number 8!

我們可以像之前一樣,放更多的東西進去。

fn main() {
    println!("Hello, worlds number {} and {}!", 8, 9);
}

這將打印出 Hello, worlds number 8 and 9!

現在我們來創建函數。

fn number() -> i32 {
    8
}

fn main() {
    println!("Hello, world number {}!", number());
}

這也會打印出 Hello, world number 8!。當Rust查看number()時,它看到一個函數。這個函數:

  • 沒有參數(因為它有())
  • 返回一個i32->(稱為 "瘦箭")顯示了函數返回的內容

函數內部只有8。因為沒有;,所以這就是它返回的值。如果它有一個;,它將不會返回任何東西(它會返回一個())。如果它有 ;,Rust 不會編譯通過,因為需要返回的是 i32,而 ; 返回 (),不是 i32

fn main() {
    println!("Hello, world number {}", number());
}

fn number() -> i32 {
    8;  // ⚠️
}
5 | fn number() -> i32 {
  |    ------      ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
6 |     8;
  |      - help: consider removing this semicolon

這意味著 "你告訴我number()返回的是i32,但你加了一個;,所以它什麼都不返回"。所以編譯器建議去掉分號。

你也可以寫return 8;,但在Rust中,正常情況下只需將;改為return即可。

當你想給一個函數賦予變量時,把它們放在()裡面。你必須給它們起個名字,寫上類型。

fn multiply(number_one: i32, number_two: i32) { // Two i32s will enter the function. We will call them number_one and number_two.
    let result = number_one * number_two;
    println!("{} times {} is {}", number_one, number_two, result);
}

fn main() {
    multiply(8, 9); // We can give the numbers directly
    let some_number = 10; // Or we can declare two variables
    let some_other_number = 2;
    multiply(some_number, some_other_number); // and put them in the function
}

我們也可以返回一個i32。只要把最後的分號去掉就可以了:

fn multiply(number_one: i32, number_two: i32) -> i32 {
    let result = number_one * number_two;
    println!("{} times {} is {}", number_one, number_two, result);
    result // this is the i32 that we return
}

fn main() {
    let multiply_result = multiply(8, 9); // We used multiply() to print and to give the result to multiply_result
}

聲明變量和代碼塊

使用let聲明一個變量(聲明一個變量=告訴Rust創建一個變量)。

fn main() {
    let my_number = 8;
    println!("Hello, number {}", my_number);
}

變量在代碼塊{}內開始和結束。在這個例子中,my_number在我們調用println!之前結束,因為它在自己的代碼塊裡面。

fn main() {
    {
        let my_number = 8; // my_number starts here
                           // my_number ends here!
    }

    println!("Hello, number {}", my_number); // ⚠️ there is no my_number and
                                             // println!() can't find it
}

你可以使用代碼塊來返回一個值。

fn main() {
    let my_number = {
    let second_number = 8;
        second_number + 9 // No semicolon, so the code block returns 8 + 9.
                          // It works just like a function
    };

    println!("My number is: {}", my_number);
}

如果在代碼塊內部添加分號,它將返回 () (無)。

fn main() {
    let my_number = {
    let second_number = 8; // declare second_number,
        second_number + 9; // add 9 to second_number
                           // but we didn't return it!
                           // second_number dies now
    };

    println!("My number is: {:?}", my_number); // my_number is ()
}

那麼為什麼我們要寫{:?}而不是{}呢?我們現在就來談談這個問題。

顯示和調試

Rust中簡單的變量可以用{}裡面的println!打印。但是有些變量不能,你需要debug print。Debug打印是給程序員打印的,因為它通常會顯示更多的信息。Debug有時看起來並不漂亮,因為它有額外的信息來幫助你。

你怎麼知道你是否需要{:?}而不是{}?編譯器會告訴你。比如說

fn main() {
    let doesnt_print = ();
    println!("This will not print: {}", doesnt_print); // ⚠️
}

當我們運行這個時,編譯器會說:

error[E0277]: `()` doesn't implement `std::fmt::Display`
 --> src\main.rs:3:41
  |
3 |     println!("This will not print: {}", doesnt_print);
  |                                         ^^^^^^^^^^^^ `()` cannot be formatted with the default formatter
  |
  = help: the trait `std::fmt::Display` is not implemented for `()`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
  = note: required by `std::fmt::Display::fmt`
  = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

信息比較多,但重要的部分是 you may be able to use {:?} (or {:#?} for pretty-print) instead. 這意味著你可以試試{:?},也可以試試{:#?} {:#?}叫做 "漂亮打印"。它和{:?}一樣,但是在更多的行上打印出不同的格式。

所以Display就是用{}打印,Debug就是用{:?}打印。

還有一點:如果你不想要新的一行,你也可以使用print!而不用ln

fn main() {
    print!("This will not print a new line");
    println!(" so this will be on the same line");
}

這將打印This will not print a new line so this will be on the same line

最小和最大的數

如果你想看最小和最大的數字,你可以用MIN和MAX。std的意思是 "標準庫",擁有Rust的所有主要函數等。我們將在以後學習標準庫。但與此同時,你可以記住,這就是你如何獲得一個類型的最小和最大的數字。

fn main() {
    println!("The smallest i8 is {} and the biggest i8 is {}.", std::i8::MIN, std::i8::MAX); // hint: printing std::i8::MIN means "print MIN inside of the i8 section in the standard library"
    println!("The smallest u8 is {} and the biggest u8 is {}.", std::u8::MIN, std::u8::MAX);
    println!("The smallest i16 is {} and the biggest i16 is {}.", std::i16::MIN, std::i16::MAX);
    println!("The smallest u16 is {} and the biggest u16 is {}.", std::u16::MIN, std::u16::MAX);
    println!("The smallest i32 is {} and the biggest i32 is {}.", std::i32::MIN, std::i32::MAX);
    println!("The smallest u32 is {} and the biggest u32 is {}.", std::u32::MIN, std::u32::MAX);
    println!("The smallest i64 is {} and the biggest i64 is {}.", std::i64::MIN, std::i64::MAX);
    println!("The smallest u64 is {} and the biggest u64 is {}.", std::u64::MIN, std::u64::MAX);
    println!("The smallest i128 is {} and the biggest i128 is {}.", std::i128::MIN, std::i128::MAX);
    println!("The smallest u128 is {} and the biggest u128 is {}.", std::u128::MIN, std::u128::MAX);

}

將會打印:

The smallest i8 is -128 and the biggest i8 is 127.
The smallest u8 is 0 and the biggest u8 is 255.
The smallest i16 is -32768 and the biggest i16 is 32767.
The smallest u16 is 0 and the biggest u16 is 65535.
The smallest i32 is -2147483648 and the biggest i32 is 2147483647.
The smallest u32 is 0 and the biggest u32 is 4294967295.
The smallest i64 is -9223372036854775808 and the biggest i64 is 9223372036854775807.
The smallest u64 is 0 and the biggest u64 is 18446744073709551615.
The smallest i128 is -170141183460469231731687303715884105728 and the biggest i128 is 170141183460469231731687303715884105727.
The smallest u128 is 0 and the biggest u128 is 340282366920938463463374607431768211455.

可變性

當你用let聲明一個變量時,它是不可改變的(不能改變)。

這將無法工作:

fn main() {
    let my_number = 8;
    my_number = 10; // ⚠️
}

編譯器說:error[E0384]: cannot assign twice to immutable variable my_number。這是因為如果你只寫let,變量是不可變的。

但有時你想改變你的變量。要創建一個可以改變的變量,就在let後面加上mut

fn main() {
    let mut my_number = 8;
    my_number = 10;
}

現在沒有問題了。

但是,你不能改變類型:甚至mut也不能讓你這樣做:這將無法工作。

fn main() {
    let mut my_variable = 8; // it is now an i32. That can't be changed
    my_variable = "Hello, world!"; // ⚠️
}

你會看到編譯器發出的同樣的 "預期"信息。expected integer, found &str. &str是一個字符串類型,我們很快就會知道。

遮蔽

shadowing是指使用let聲明一個與另一個變量同名的新變量。它看起來像可變性,但完全不同。shadowing看起來是這樣的:

fn main() {
    let my_number = 8; // This is an i32
    println!("{}", my_number); // prints 8
    let my_number = 9.2; // This is an f64 with the same name. But it's not the first my_number - it is completely different!
    println!("{}", my_number) // Prints 9.2
}

這裡我們說我們用一個新的 "let綁定"對my_number進行了 "shadowing"。

那麼第一個my_number是否被銷燬了呢?沒有,但是當我們調用my_number時,我們現在得到my_numberf64。因為它們在同一個作用域塊中(同一個 {}),我們不能再看到第一個 my_number

但如果它們在不同的塊中,我們可以同時看到兩個。 例如:

fn main() {
    let my_number = 8; // This is an i32
    println!("{}", my_number); // prints 8
    {
        let my_number = 9.2; // This is an f64. It is not my_number - it is completely different!
        println!("{}", my_number) // Prints 9.2
                                  // But the shadowed my_number only lives until here.
                                  // The first my_number is still alive!
    }
    println!("{}", my_number); // prints 8
}

因此,當你對一個變量進行shadowing處理時,你不會破壞它。你屏蔽了它。

那麼shadowing的好處是什麼呢?當你需要經常改變一個變量的時候,shadowing是很好的。想象一下,你想用一個變量做很多簡單的數學運算。

fn times_two(number: i32) -> i32 {
    number * 2
}

fn main() {
    let final_number = {
        let y = 10;
        let x = 9; // x starts at 9
        let x = times_two(x); // shadow with new x: 18
        let x = x + y; // shadow with new x: 28
        x // return x: final_number is now the value of x
    };
    println!("The number is now: {}", final_number)
}

如果沒有shadowing,你將不得不考慮不同的名稱,儘管你並不關心x。

fn times_two(number: i32) -> i32 {
    number * 2
}

fn main() {
    // Pretending we are using Rust without shadowing
    let final_number = {
        let y = 10;
        let x = 9; // x starts at 9
        let x_twice = times_two(x); // second name for x
        let x_twice_and_y = x_twice + y; // third name for x!
        x_twice_and_y // too bad we didn't have shadowing - we could have just used x
    };
    println!("The number is now: {}", final_number)
}

一般來說,你在Rust中看到的shadowing就是這種情況。它發生在你想快速取用變量,對它做一些事情,然後再做其他事情的地方。而你通常將它用於那些你不太關心的快速變量。

棧、堆和指針

棧、堆和指針在Rust中非常重要。

棧和堆是計算機中保存內存的兩個地方。重要的區別是:

棧的速度非常快, 但堆的速度就不那麼快了. 它也不是超慢,但棧總是更快。但是你不能一直使用棧,因為:

  • Rust需要在編譯時知道一個變量的大小。所以像i32這樣的簡單變量就放在堆棧上,因為我們知道它們的確切大小。你總是知道i32要4字節,因為32位=4字節。所以i32總是可以放在棧上。
  • 但有些類型在編譯時不知道大小。但是棧需要知道確切的大小。那麼你該怎麼做呢?首先你把數據放在堆中,因為堆中可以有任何大小的數據。然後為了找到它,一個指針就會進入棧。這很好,因為我們總是知道指針的大小。所以,計算機就會先去棧,讀取指針,然後跟著指針到數據所在的堆。

指針聽起來很複雜,但它們很容易。指針就像一本書的目錄。想象一下這本書。

MY BOOK

TABLE OF CONTENTS

Chapter                        Page
Chapter 1: My life              1
Chapter 2: My cat               15
Chapter 3: My job               23
Chapter 4: My family            30
Chapter 5: Future plans         43

所以這就像五個指針。你可以閱讀它們,找到它們所說的信息。"我的生活"這一章在哪裡?在第1頁(它指向第1頁)。"我的工作"這一章在哪裡?它在第23頁。

在Rust中通常看到的指針叫做引用。這是重要的部分,要知道:一個引用指向另一個值的內存。引用意味著你了這個值,但你並不擁有它。這和我們的書一樣:目錄並不擁有信息。章節才是信息的主人。在Rust中,引用文獻的前面有一個&。所以:

  • let my_variable = 8是一個普通的變量,但是:
  • let my_reference = &my_variable是一個引用。

你把 my_reference = &my_variable 讀成這樣: "my_reference是對my_variable的引用". 或者:"my_reference是對my_variable的引用"。

這意味著my_reference只看my_variable的數據。my_variable仍然擁有它的數據。

你也可以有一個引用的引用,或者任何數量的引用。

fn main() {
    let my_number = 15; // This is an i32
    let single_reference = &my_number; //  This is a &i32
    let double_reference = &single_reference; // This is a &&i32
    let five_references = &&&&&my_number; // This is a &&&&&i32
}

這些都是不同的類型,就像 "朋友的朋友"和 "朋友"不同一樣。

關於打印的更多信息

在Rust中,你幾乎可以用任何你想要的方式打印東西。這裡有一些關於打印的事情需要知道。

添加 \n 將會產生一個新行,而 \t 將會產生一個標籤。

fn main() {
    // Note: this is print!, not println!
    print!("\t Start with a tab\nand move to a new line");
}

這樣就可以打印了。

         Start with a tab
and move to a new line

""裡面可以寫過很多行都沒有問題,但是要注意間距。

fn main() {
    // Note: After the first line you have to start on the far left.
    // If you write directly under println!, it will add the spaces
    println!("Inside quotes
you can write over
many lines
and it will print just fine.");

    println!("If you forget to write
    on the left side, the spaces
    will be added when you print.");
}

這個打印出來的。

Inside quotes
you can write over
many lines
and it will print just fine.
If you forget to write
    on the left side, the spaces
    will be added when you print.

如果你想打印\n這樣的字符(稱為 "轉義字符"),你可以多加一個\

fn main() {
    println!("Here are two escape characters: \\n and \\t");
}

這樣就可以打印了。

Here are two escape characters: \n and \t

有時你有太多的 " 和轉義字符,並希望 Rust 忽略所有的字符。要做到這一點,您可以在開頭添加 r#,在結尾添加 #

fn main() {
    println!("He said, \"You can find the file at c:\\files\\my_documents\\file.txt.\" Then I found the file."); // We used \ five times here
    println!(r#"He said, "You can find the file at c:\files\my_documents\file.txt." Then I found the file."#)
}

這打印的是同樣的東西,但使用 r# 使人類更容易閱讀。

He said, "You can find the file at c:\files\my_documents\file.txt." Then I found the file.
He said, "You can find the file at c:\files\my_documents\file.txt." Then I found the file.

如果你需要在裡面打印#,那麼你可以用r##開頭,用##結尾。如果你需要打印多個連續的#,可以在每邊多加一個#。

下面是四個例子。

fn main() {

    let my_string = "'Ice to see you,' he said."; // single quotes
    let quote_string = r#""Ice to see you," he said."#; // double quotes
    let hashtag_string = r##"The hashtag #IceToSeeYou had become very popular."##; // Has one # so we need at least ##
    let many_hashtags = r####""You don't have to type ### to use a hashtag. You can just use #.""####; // Has three ### so we need at least ####

    println!("{}\n{}\n{}\n{}\n", my_string, quote_string, hashtag_string, many_hashtags);

}

這將打印:

'Ice to see you,' he said.
"Ice to see you," he said.
The hashtag #IceToSeeYou had become very popular.
"You don't have to type ### to use a hashtag. You can just use #."

r#還有另一個用途:使用它,你可以使用關鍵字(如letfn等)作為變量名。

fn main() {
    let r#let = 6; // The variable's name is let
    let mut r#mut = 10; // This variable's name is mut
}

r#之所以有這個功能,是因為舊版本的Rust的關鍵字比現在的Rust少。所以有了r#就可以避免以前不是關鍵字的變量名的錯誤。

又或者因為某些原因,你確實需要一個函數的名字,比如return。那麼你可以這樣寫:

fn r#return() -> u8 {
    println!("Here is your number.");
    8
}

fn main() {
    let my_number = r#return();
    println!("{}", my_number);
}

這樣打印出來的結果是:

Here is your number.
8

所以你可能不需要它,但是如果你真的需要為一個變量使用一個關鍵字,那麼你可以使用r#

如果你想打印&strchar的字節,你可以在字符串前寫上b就可以了。這適用於所有ASCII字符。這些是所有的ASCII字符。

☺☻♥♦♣♠♫☼►◄↕‼¶§▬↨↑↓→∟↔▲▼123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~

所以,當你打印這個

fn main() {
    println!("{:?}", b"This will look like numbers");
}

這就是結果:

[84, 104, 105, 115, 32, 119, 105, 108, 108, 32, 108, 111, 111, 107, 32, 108, 105, 107, 101, 32, 110, 117, 109, 98, 101, 114, 115]

對於char來說,這叫做一個字節,對於&str來說,這叫做一個字節字符串

如果你需要的話,也可以把br放在一起。

fn main() {
    println!("{:?}", br##"I like to write "#"."##);
}

這將打印出 [73, 32, 108, 105, 107, 101, 32, 116, 111, 32, 119, 114, 105, 116, 101, 32, 34, 35, 34, 46]

還有一個Unicode轉義,可以讓你在字符串中打印任何Unicode字符: \u{}{}裡面有一個十六進制數字可以打印。下面是一個簡短的例子,說明如何獲得Unicode數字,以及如何再次打印它。

fn main() {
    println!("{:X}", '행' as u32); // Cast char as u32 to get the hexadecimal value
    println!("{:X}", 'H' as u32);
    println!("{:X}", '居' as u32);
    println!("{:X}", 'い' as u32);

    println!("\u{D589}, \u{48}, \u{5C45}, \u{3044}"); // Try printing them with unicode escape \u
}

我們知道,println!可以和{}(用於顯示)或{:?}(用於調試)一起打印,再加上{:#?}就可以進行漂亮的打印。但是還有很多其他的打印方式。

例如,如果你有一個引用,你可以用{:p}來打印指針地址。指針地址指的是電腦內存中的位置。

fn main() {
    let number = 9;
    let number_ref = &number;
    println!("{:p}", number_ref);
}

這可以打印0xe2bc0ffcfc或其他地址。每次可能都不一樣,這取決於你的計算機存儲的位置。

或者你可以打印二進制、十六進制和八進制。

fn main() {
    let number = 555;
    println!("Binary: {:b}, hexadecimal: {:x}, octal: {:o}", number, number, number);
}

這將打印出Binary: 1000101011, hexadecimal: 22b, octal: 1053

或者你可以添加數字來改變順序。第一個變量將在索引0中,下一個在索引1中,以此類推。

fn main() {
    let father_name = "Vlad";
    let son_name = "Adrian Fahrenheit";
    let family_name = "Țepeș";
    println!("This is {1} {2}, son of {0} {2}.", father_name, son_name, family_name);
}

father_name在0位,son_name在1位,family_name在2位。所以它打印的是This is Adrian Fahrenheit Țepeș, son of Vlad Țepeș

也許你有一個非常複雜的字符串要打印,{}大括號內有太多的變量。或者你需要不止一次的打印一個變量。那麼在{}中添加名稱就會有幫助。

fn main() {
    println!(
        "{city1} is in {country} and {city2} is also in {country},
but {city3} is not in {country}.",
        city1 = "Seoul",
        city2 = "Busan",
        city3 = "Tokyo",
        country = "Korea"
    );
}

這樣就可以打印了。

Seoul is in Korea and Busan is also in Korea,
but Tokyo is not in Korea.

如果你願意,也可以在Rust中進行非常複雜的打印。下面展示怎樣做:

{variable:padding alignment minimum.maximum}

要理解這一點,請看

  1. 你想要一個變量名嗎?先寫出來,就像我們上面寫{country}一樣。 (如果你想做更多的事情,就在後面加一個:)
  2. 你想要一個填充字符嗎?例如,55加上三個 "填充零"就像00055。
  3. padding的對齊方式(左/中/右)?
  4. 你想要一個最小長度嗎?(寫一個數字就可以了)
  5. 你想要一個最大長度嗎?(寫一個數字,前面有一個.)

例如,我想寫 "a",左邊有五個ㅎ,右邊有五個ㅎ。

fn main() {
    let letter = "a";
    println!("{:ㅎ^11}", letter);
}

這樣打印出來的結果是ㅎㅎㅎㅎㅎaㅎㅎㅎㅎㅎ。我們看看1)到5)的這個情況,就能明白編譯器是怎麼解讀的:

  • 你要不要變量名?{:ㅎ^11}沒有變量名。:之前沒有任何內容。
  • 你需要一個填充字符嗎?{:ㅎ^11} 是的:ㅎ"在:後面,有一個^<表示變量在填充字符左邊,>表示在填充字符右邊,^表示在填充字符中間。
  • 要不要設置最小長度?{:ㅎ^11}是:後面有一個11。
  • 你想要一個最大長度嗎?{:ㅎ^11} 不是:前面沒有.的數字。

下面是多種類型的格式化的例子:

fn main() {
    let title = "TODAY'S NEWS";
    println!("{:-^30}", title); // no variable name, pad with -, put in centre, 30 characters long
    let bar = "|";
    println!("{: <15}{: >15}", bar, bar); // no variable name, pad with space, 15 characters each, one to the left, one to the right
    let a = "SEOUL";
    let b = "TOKYO";
    println!("{city1:-<15}{city2:->15}", city1 = a, city2 = b); // variable names city1 and city2, pad with -, one to the left, one to the right
}

它打印出來了。

---------TODAY'S NEWS---------
|                            |
SEOUL--------------------TOKYO

字符串

Rust有兩種主要類型的字符串。String&str. 有什麼區別呢?

  • &str是一個簡單的字符串。當你寫let my_variable = "Hello, world!"時,你會創建一個&str&str是非常快的。
  • String是一個更復雜的字符串。它比較慢,但它有更多的功能。String是一個指針,數據在堆上。

另外注意,&str前面有&,因為你需要一個引用來使用str。這是因為我們上面看到的原因:堆需要知道大小。所以我們給它一個&,它知道大小,然後它就高興了。另外,因為你用一個&與一個str交互,你並不擁有它。但是一個String是一個擁有的類型。我們很快就會知道為什麼這一點很重要。

&strString都是UTF-8。例如,你可以寫

fn main() {
    let name = "서태지"; // This is a Korean name. No problem, because a &str is UTF-8.
    let other_name = String::from("Adrian Fahrenheit Țepeș"); // Ț and ș are no problem in UTF-8.
}

你可以在String::from("Adrian Fahrenheit Țepeș")中看到,很容易從&str中創建一個String。這兩種類型雖然不同,但聯繫非常緊密。

你甚至可以寫表情符號,這要感謝UTF-8。

fn main() {
    let name = "😂";
    println!("My name is actually {}", name);
}

在你的電腦上,會打印My name is actually 😂,除非你的命令行不能打印。那麼它會顯示My name is actually �。但Rust對emojis或其他Unicode沒有問題。

我們再來看看str使用&的原因,以確保我們理解。

  • str是一個動態大小的類型(動態大小=大小可以不同)。比如 "서태지"和 "Adrian Fahrenheit Țepeș"這兩個名字的大小是不一樣的。
fn main() {

    println!("A String is always {:?} bytes. It is Sized.", std::mem::size_of::<String>()); // std::mem::size_of::<Type>() gives you the size in bytes of a type
    println!("And an i8 is always {:?} bytes. It is Sized.", std::mem::size_of::<i8>());
    println!("And an f64 is always {:?} bytes. It is Sized.", std::mem::size_of::<f64>());
    println!("But a &str? It can be anything. '서태지' is {:?} bytes. It is not Sized.", std::mem::size_of_val("서태지")); // std::mem::size_of_val() gives you the size in bytes of a variable
    println!("And 'Adrian Fahrenheit Țepeș' is {:?} bytes. It is not Sized.", std::mem::size_of_val("Adrian Fahrenheit Țepeș"));
}

這個打印:

A String is always 24 bytes. It is Sized.
And an i8 is always 1 bytes. It is Sized.
And an f64 is always 8 bytes. It is Sized.
But a &str? It can be anything. '서태지' is 9 bytes. It is not Sized.
And 'Adrian Fahrenheit Țepeș' is 25 bytes. It is not Sized.

這就是為什麼我們需要一個 &,因為 & 是一個指針,而 Rust 知道指針的大小。所以指針會放在棧中。如果我們寫str,Rust就不知道該怎麼做了,因為它不知道指針的大小。

有很多方法可以創建String。下面是一些。

  • String::from("This is the string text"); 這是String的一個方法,它接受文本並創建一個String.
  • "This is the string text".to_string(). 這是&str的一個方法,使其成為一個String。
  • format! 宏。 這和println!一樣,只是它創建了一個字符串,而不是打印。所以你可以這樣做:
fn main() {
    let my_name = "Billybrobby";
    let my_country = "USA";
    let my_home = "Korea";

    let together = format!(
        "I am {} and I come from {} but I live in {}.",
        my_name, my_country, my_home
    );
}

現在我們有了一個一起命名的字符串,但還沒有打印出來。

還有一種創建String的方法叫做.into(),但它有點不同,因為.into()並不只是用來創建String。有些類型可以很容易地使用From.into()轉換為另一種類型,並從另一種類型轉換出來。而如果你有From,那麼你也有.into()From 更加清晰,因為你已經知道了類型:你知道 String::from("Some str") 是一個來自 &strString。但是對於.into(),有時候編譯器並不知道。

fn main() {
    let my_string = "Try to make this a String".into(); // ⚠️
}

Rust不知道你要的是什麼類型,因為很多類型都可以從一個&str創建出來。它說:"我可以把一個&str做成很多東西。你想要哪一種?"

error[E0282]: type annotations needed
 --> src\main.rs:2:9
  |
2 |     let my_string = "Try to make this a String".into();
  |         ^^^^^^^^^ consider giving `my_string` a type

所以你可以這樣做:

fn main() {
    let my_string: String = "Try to make this a String".into();
}

現在你得到了一個字符串。

const和static

有兩種聲明值的方法,不僅僅是用let。它們是conststatic。另外,Rust不會使用類型推理:你需要為它們編寫類型。這些都是用於不改變的值(const意味著常量)。區別在於:

  • const是用於不改變的值,當使用它時,名字會被替換成值。
  • staticconst類似,但有一個固定的內存位置,可以作為一個全局變量使用。

所以它們幾乎是一樣的。Rust程序員幾乎總是使用const

一般用全大寫字母作為名字,而且通常在main之外,這樣它們就可以在整個程序中生存。

兩個例子是 const NUMBER_OF_MONTHS: u32 = 12;static SEASONS: [&str; 4] = ["Spring", "Summer", "Fall", "Winter"];

關於引用的更多信息

引用在Rust中非常重要。Rust使用引用來確保所有的內存訪問是安全的。我們知道,我們使用&來創建一個引用。

fn main() {
    let country = String::from("Austria");
    let ref_one = &country;
    let ref_two = &country;

    println!("{}", ref_one);
}

這樣就會打印出Austria

在代碼中,country是一個String。然後我們創建了兩個country的引用。它們的類型是&String,你說這是一個 "字符串的引用"。我們可以創建三個引用或者一百個對 country 的引用,這都沒有問題。

但這是一個問題。

fn return_str() -> &str {
    let country = String::from("Austria");
    let country_ref = &country;
    country_ref // ⚠️
}

fn main() {
    let country = return_str();
}

return_str()函數創建了一個String,然後它創建了一個對String的引用。然後它試圖返回引用。但是country這個String只活在函數裡面,然後它就死了。一旦一個變量消失了,計算機就會清理內存,並將其用於其他用途。所以在函數結束後,country_ref引用的是已經消失的內存,這是不對的。Rust防止我們在這裡犯內存的錯誤。

這就是我們上面講到的 "擁有"類型的重要部分。因為你擁有一個String,你可以把它傳給別人。但是如果 &StringString 死了,那麼 &String 就會死掉,所以你不能把它的 "所有權"傳給別人。

可變引用

如果您想使用一個引用來改變數據,您可以使用一個可變引用。對於可變引用,您可以寫 &mut 而不是 &

fn main() {
    let mut my_number = 8; // don't forget to write mut here!
    let num_ref = &mut my_number;
}

那麼這兩種類型是什麼呢?my_numberi32num_ref&mut i32(我們說是 "可變引用i32")。

所以我們用它來給my_number加10。但是你不能寫num_ref += 10,因為num_ref不是i32的值,它是一個&i32。其實這個值就在i32裡面。為了達到值所在的地方,我們用**的意思是 "我不要引用,我要引用對應的值"。換句話說,一個*&是相反的。另外,一個*抹去了一個&

fn main() {
    let mut my_number = 8;
    let num_ref = &mut my_number;
    *num_ref += 10; // Use * to change the i32 value.
    println!("{}", my_number);

    let second_number = 800;
    let triple_reference = &&&second_number;
    println!("Second_number = triple_reference? {}", second_number == ***triple_reference);
}

這個打印:

18
Second_number = triple_reference? true

因為使用&叫做 "引用",所以使用*叫做 "dereferencing"。

Rust有兩個規則,分別是可變引用和不可變引用。它們非常重要,但也很容易記住,因為它們很有意義。

  • 規則1。如果你只有不可變引用,你可以有任意多的引用。1個也行,3個也行,1000個也行,沒問題。
  • 規則2: 如果你有一個可變引用,你只能有一個。另外,你不能同時使用一個不可變引用一個可變引用。

這是因為可變引用可以改變數據。如果你在其他引用讀取數據時改變數據,你可能會遇到問題。

一個很好的理解方式是思考一個Powerpoint演示。

情況一是關於只有一個可變引用

情境一 一個員工正在編寫一個Powerpoint演示文稿,他希望他的經理能幫助他。他希望他的經理能幫助他。該員工將自己的登錄信息提供給經理,並請他幫忙進行編輯。現在,經理對該員工的演示文稿有了一個 "可變引用"。經理可以做任何他想做的修改,然後把電腦還給他。這很好,因為沒有人在看這個演示文稿。

情況二是關於只有不可變引用

情況二 該員工要給100個人做演示。現在這100個人都可以看到該員工的數據。 他們都有一個 "不可改變的引用",即員工的介紹。這很好,因為他們可以看到它,但沒有人可以改變數據。

情況三是有問題的情況

情況三 員工把他的登錄信息給了經理 他的經理現在有了一個 "可變引用"。然後員工去給100個人做演示,但是經理還是可以登錄。這是不對的,因為經理可以登錄,可以做任何事情。也許他的經理會登錄電腦,然後開始給他的母親打一封郵件! 現在這100人不得不看著經理給他母親寫郵件,而不是演示。這不是他們期望看到的。

下面是一個可變借用與不可變借用的例子:

fn main() {
    let mut number = 10;
    let number_ref = &number;
    let number_change = &mut number;
    *number_change += 10;
    println!("{}", number_ref); // ⚠️
}

編譯器打印了一個有用的信息來告訴我們問題所在。

error[E0502]: cannot borrow `number` as mutable because it is also borrowed as immutable
 --> src\main.rs:4:25
  |
3 |     let number_ref = &number;
  |                      ------- immutable borrow occurs here
4 |     let number_change = &mut number;
  |                         ^^^^^^^^^^^ mutable borrow occurs here
5 |     *number_change += 10;
6 |     println!("{}", number_ref);
  |                    ---------- immutable borrow later used here

然而,這段代碼可以工作。為什麼會這樣?

fn main() {
    let mut number = 10;
    let number_change = &mut number; // create a mutable reference
    *number_change += 10; // use mutable reference to add 10
    let number_ref = &number; // create an immutable reference
    println!("{}", number_ref); // print the immutable reference
}

它打印出20沒有問題。它能工作是因為編譯器足夠聰明,能夠理解我們的代碼。它知道我們使用了number_change來改變number,但沒有再使用它。所以這裡沒有問題。我們並沒有將不可變和可變引用一起使用。

早期在Rust中,這種代碼實際上會產生錯誤,但現在的編譯器更聰明瞭。它不僅能理解我們輸入的內容,還能理解我們如何使用所有的東西。

再談shadowing

還記得我們說過,shadowing不會破壞一個值,而是屏蔽它嗎?現在我們可以用引用來看看這個問題。

fn main() {
    let country = String::from("Austria");
    let country_ref = &country;
    let country = 8;
    println!("{}, {}", country_ref, country);
}

這是打印Austria, 8還是8, 8?它打印的是Austria, 8。首先我們聲明一個String,叫做country。然後我們給這個字符串創建一個引用country_ref。然後我們用8來shadowing國家,這是一個i32。但是第一個country並沒有被銷燬,所以country_ref仍然寫著 "Austria",而不是 "8"。下面是同樣的代碼,並加了一些註釋來說明它的工作原理。

fn main() {
    let country = String::from("Austria"); // Now we have a String called country
    let country_ref = &country; // country_ref is a reference to this data. It's not going to change
    let country = 8; // Now we have a variable called country that is an i8. But it has no relation to the other one, or to country_ref
    println!("{}, {}", country_ref, country); // country_ref still refers to the data of String::from("Austria") that we gave it.
}

函數的引用

引用對函數非常有用。Rust中關於值的規則是:一個值只能有一個所有者。

這段代碼將無法工作:

fn print_country(country_name: String) {
    println!("{}", country_name);
}

fn main() {
    let country = String::from("Austria");
    print_country(country); // We print "Austria"
    print_country(country); // ⚠️ That was fun, let's do it again!
}

它不能工作,因為country被破壞了。下面是如何操作的。

  • 第一步,我們創建String,稱為countrycountry是所有者。
  • 第二步:我們把countryprint_countryprint_country沒有->,所以它不返回任何東西。print_country完成後,我們的String現在已經死了。
  • 第三步:我們嘗試把countryprint_country,但我們已經這樣做了。我們已經沒有country可以給了。

我們可以讓print_countryString回來,但是有點尷尬。

fn print_country(country_name: String) -> String {
    println!("{}", country_name);
    country_name // return it here
}

fn main() {
    let country = String::from("Austria");
    let country = print_country(country); // we have to use let here now to get the String back
    print_country(country);
}

現在打印出來了。

Austria
Austria

更好的解決方法是增加&

fn print_country(country_name: &String) {
    println!("{}", country_name);
}

fn main() {
    let country = String::from("Austria");
    print_country(&country); // We print "Austria"
    print_country(&country); // That was fun, let's do it again!
}

現在 print_country() 是一個函數,它接受 String 的引用: &String。另外,我們給country一個引用,寫作&country。這表示 "你可以看它,但我要保留它"。

現在讓我們用一個可變引用來做類似的事情。下面是一個使用可變變量的函數的例子:

fn add_hungary(country_name: &mut String) { // first we say that the function takes a mutable reference
    country_name.push_str("-Hungary"); // push_str() adds a &str to a String
    println!("Now it says: {}", country_name);
}

fn main() {
    let mut country = String::from("Austria");
    add_hungary(&mut country); // we also need to give it a mutable reference.
}

此打印Now it says: Austria-Hungary

所以得出結論:

  • fn function_name(variable: String)接收了String,並擁有它。如果它不返回任何東西,那麼這個變量就會在函數裡面死亡。
  • fn function_name(variable: &String) 借用 String 並可以查看它
  • fn function_name(variable: &mut String)借用String,可以更改。

下面是一個看起來像可變引用的例子,但它是不同的。

fn main() {
    let country = String::from("Austria"); // country is not mutable, but we are going to print Austria-Hungary. How?
    adds_hungary(country);
}

fn adds_hungary(mut country: String) { // Here's how: adds_hungary takes the String and declares it mutable!
    country.push_str("-Hungary");
    println!("{}", country);
}

這怎麼可能呢?因為mut country不是引用。adds_hungary現在擁有country。(記住,它佔用的是String而不是&String)。當你調用adds_hungary的那一刻,它就完全成了country的主人。countryString::from("Austria")沒有關係了。所以,adds_hungary可以把country當作可變的,這樣做是完全安全的。

還記得我們上面的員工Powerpoint和經理的情況嗎?在這種情況下,就好比員工只是把自己的整臺電腦交給了經理。員工不會再碰它,所以經理可以對它做任何他想做的事情。

拷貝類型

Rust中的一些類型非常簡單。它們被稱為拷貝類型。這些簡單的類型都在棧中,編譯器知道它們的大小。這意味著它們非常容易複製,所以當你把它發送到一個函數時,編譯器總是會複製。它總是複製,因為它們是如此的小而簡單,沒有理由不復制。所以你不需要擔心這些類型的所有權問題。

這些簡單的類型包括:整數、浮點數、布爾值(truefalse)和char

如何知道一個類型是否實現複製?(實現 = 能夠使用)你可以查看文檔。例如,這裡是 char 的文檔:

https://doc.rust-lang.org/std/primitive.char.html

在左邊你可以看到Trait Implementations。例如你可以看到Copy, Debug, 和 Display。所以你知道,當你把一個char:

  • 當你把它發送到一個函數(Copy)時,它就被複制了。
  • 可以用{}打印(Display)
  • 可以用{:?}打印(Debug)
fn prints_number(number: i32) { // There is no -> so it's not returning anything
                             // If number was not copy type, it would take it
                             // and we couldn't use it again
    println!("{}", number);
}

fn main() {
    let my_number = 8;
    prints_number(my_number); // Prints 8. prints_number gets a copy of my_number
    prints_number(my_number); // Prints 8 again.
                              // No problem, because my_number is copy type!
}

但是如果你看一下String的文檔,它不是拷貝類型。

https://doc.rust-lang.org/std/string/struct.String.html

在左邊的Trait Implementations中,你可以按字母順序查找。A、B、C......C中沒有Copy,但是有CloneCloneCopy類似,但通常需要更多的內存。另外,你必須用.clone()來調用它--它不會自己克隆。

在這個例子中,prints_country()打印的是國家名稱,一個String。我們想打印兩次,但我們不能。

fn prints_country(country_name: String) {
    println!("{}", country_name);
}

fn main() {
    let country = String::from("Kiribati");
    prints_country(country);
    prints_country(country); // ⚠️
}

但現在我們明白了這個信息。

error[E0382]: use of moved value: `country`
 --> src\main.rs:4:20
  |
2 |     let country = String::from("Kiribati");
  |         ------- move occurs because `country` has type `std::string::String`, which does not implement the `Copy` trait
3 |     prints_country(country);
  |                    ------- value moved here
4 |     prints_country(country);
  |                    ^^^^^^^ value used here after move

重要的部分是which does not implement the Copy trait。但是在文檔中我們看到String實現了Clone的特性。所以我們可以在代碼中添加.clone()。這樣就創建了一個克隆,然後我們將克隆發送到函數中。現在 country 還活著,所以我們可以使用它。

fn prints_country(country_name: String) {
    println!("{}", country_name);
}

fn main() {
    let country = String::from("Kiribati");
    prints_country(country.clone()); // make a clone and give it to the function. Only the clone goes in, and country is still alive
    prints_country(country);
}

當然,如果String非常大,.clone()就會佔用很多內存。一個String可以是一整本書的長度,我們每次調用.clone()都會複製這本書。所以,如果可以的話,使用&來做引用是比較快的。例如,這段代碼將&str推送到String上,然後每次在函數中使用時都會進行克隆。

fn get_length(input: String) { // Takes ownership of a String
    println!("It's {} words long.", input.split_whitespace().count()); // splits to count the number of words
}

fn main() {
    let mut my_string = String::new();
    for _ in 0..50 {
        my_string.push_str("Here are some more words "); // push the words on
        get_length(my_string.clone()); // gives it a clone every time
    }
}

它的打印。

It's 5 words long.
It's 10 words long.
...
It's 250 words long.

這就是50個克隆。這裡是用引用代替更好:

fn get_length(input: &String) {
    println!("It's {} words long.", input.split_whitespace().count());
}

fn main() {
    let mut my_string = String::new();
    for _ in 0..50 {
        my_string.push_str("Here are some more words ");
        get_length(&my_string);
    }
}

不是50個克隆,而是0個。

無值變量

一個沒有值的變量叫做 "未初始化"變量。未初始化的意思是 "還沒有開始"。它們很簡單:只需寫上let和變量名。

fn main() {
    let my_variable; // ⚠️
}

但是你還不能使用它,如果任何東西都沒有被初始化,Rust就不會編譯。

但有時它們會很有用。一個很好的例子是當:

  • 你有一個代碼塊,而你的變量值就在裡面,並且
  • 變量需要活在代碼塊之外。
fn loop_then_return(mut counter: i32) -> i32 {
    loop {
        counter += 1;
        if counter % 50 == 0 {
            break;
        }
    }
    counter
}

fn main() {
    let my_number;

    {
        // Pretend we need to have this code block
        let number = {
            // Pretend there is code here to make a number
            // Lots of code, and finally:
            57
        };

        my_number = loop_then_return(number);
    }

    println!("{}", my_number);
}

這將打印出 100

你可以看到 my_number 是在 main() 函數中聲明的,所以它一直活到最後。但是它的值是在循環裡面得到的。然而,這個值和my_number一樣長,因為my_number有這個值。而如果你在塊裡面寫了let my_number = loop_then_return(number),它就會馬上死掉。

如果你簡化代碼,對想象是有幫助的。loop_then_return(number)給出的結果是100,所以我們刪除它,改寫100。另外,現在我們不需要 number,所以我們也刪除它。現在它看起來像這樣:

fn main() {
    let my_number;
    {
        my_number = 100;
    }

    println!("{}", my_number);
}

所以說let my_number = { 100 };差不多。

另外注意,my_number不是mut。我們在給它50之前並沒有給它一個值,所以它的值一直沒有改變。最後,my_number的真正代碼只是let my_number = 100;

集合類型

Rust有很多類型用於創建集合。當你需要在一個地方有多個值時,就可以使用集合。例如,你可以在一個變量中包含你所在國家的所有城市的信息。我們先從數組開始,數組的速度最快,但功能也最少。它們在這方面有點像&str

數組

數組是方括號內的數據。[]. 數組:

  • 不能改變其大小。
  • 必須只包含相同的類型。

但是,它們的速度非常快。

數組的類型是:[type; number]。例如,["One", "Two"]的類型是[&str; 2]。這意味著,即使這兩個數組也有不同的類型。

fn main() {
    let array1 = ["One", "Two"]; // This one is type [&str; 2]
    let array2 = ["One", "Two", "Five"]; // But this one is type [&str; 3]. Different type!
}

這裡有一個很好的提示:要想知道一個變量的類型,你可以通過給編譯器下壞指令來 "詢問"它。比如說

fn main() {
    let seasons = ["Spring", "Summer", "Autumn", "Winter"];
    let seasons2 = ["Spring", "Summer", "Fall", "Autumn", "Winter"];
    seasons.ddd(); // ⚠️
    seasons2.thd(); // ⚠️ as well
}

編譯器說:"什麼?seasons沒有.ddd()的方法,seasons2也沒有.thd()的方法!!"你可以看到:

error[E0599]: no method named `ddd` found for array `[&str; 4]` in the current scope
 --> src\main.rs:4:13
  |
4 |     seasons.ddd(); // 
  |             ^^^ method not found in `[&str; 4]`

error[E0599]: no method named `thd` found for array `[&str; 5]` in the current scope
 --> src\main.rs:5:14
  |
5 |     seasons2.thd(); // 
  |              ^^^ method not found in `[&str; 5]`

所以它告訴你method not found in `[&str; 4]`,這就是類型。

如果你想要一個數值都一樣的數組,你可以這樣聲明。

fn main() {
    let my_array = ["a"; 10];
    println!("{:?}", my_array);
}

這樣就打印出了["a", "a", "a", "a", "a", "a", "a", "a", "a", "a"]

這個方法經常用來創建緩衝區。例如,let mut buffer = [0; 640]創建一個640個零的數組。然後我們可以將零改為其他數字,以便添加數據。

你可以用[]來索引(獲取)數組中的條目。第一個條目是[0],第二個是[1],以此類推。

fn main() {
    let my_numbers = [0, 10, -20];
    println!("{}", my_numbers[1]); // prints 10
}

你可以得到一個數組的一個片斷(一塊)。首先你需要一個&,因為編譯器不知道大小。然後你可以使用..來顯示範圍。

例如,讓我們使用這個數組。[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].

fn main() {
    let array_of_ten = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let three_to_five = &array_of_ten[2..5];
    let start_at_two = &array_of_ten[1..];
    let end_at_five = &array_of_ten[..5];
    let everything = &array_of_ten[..];

    println!("Three to five: {:?}, start at two: {:?}, end at five: {:?}, everything: {:?}", three_to_five, start_at_two, end_at_five, everything);
}

記住這一點。

  • 索引號從0開始(不是1)
  • 索引範圍是不包含的(不包括最後一個數字)。

所以[0..2]是指第一個指數和第二個指數(0和1)。或者你也可以稱它為 "零點和第一"指數。它沒有第三項,也就是索引2。

你也可以有一個包含的範圍,這意味著它也包括最後一個數字。要做到這一點。 添加=,寫成..=,而不是..。所以,如果你想要第一項、第二項和第三項,可以寫成[0..=2],而不是[0..2]

向量

就像我們有&strString一樣,我們有數組和向量。數組的功能少了就快,向量的功能多了就慢。(當然,Rust的速度一直都是非常快的,所以向量並不慢,只是比數組慢一點)。類型寫成Vec,你也可以直接叫它 "vec"。

向量的聲明主要有兩種方式。一種是像String一樣使用new:

fn main() {
    let name1 = String::from("Windy");
    let name2 = String::from("Gomesy");

    let mut my_vec = Vec::new();
    // If we run the program now, the compiler will give an error.
    // It doesn't know the type of vec.

    my_vec.push(name1); // Now it knows: it's Vec<String>
    my_vec.push(name2);
}

你可以看到Vec裡面總是有其他東西,這就是<>(角括號)的作用。Vec<String>是一個有一個或多個String的向量。你還可以在裡面有更多的類型。比如說

  • Vec<(i32, i32)> 這是一個 Vec 其中每個元素是一個元組。(i32, i32).
  • Vec<Vec<String>>這是一個Vec,其中有VecStrings。比如說你想把你喜歡的書保存為Vec<String>。然後你再用另一本書來做,就會得到另一個Vec<String>。為了保存這兩本書,你會把它們放入另一個Vec中,這就是Vec<Vec<String>>

與其使用 .push() 讓 Rust 決定類型,不如直接聲明類型。

fn main() {
    let mut my_vec: Vec<String> = Vec::new(); // The compiler knows the type
                                              // so there is no error.
}

你可以看到,向量中的元素必須具有相同的類型。

另一個創建向量的簡單方法是使用 vec! 宏。它看起來像一個數組聲明,但前面有 vec!

fn main() {
    let mut my_vec = vec![8, 10, 10];
}

類型是Vec<i32>。你稱它為 "i32的Vec"。而Vec<String>是 "String的Vec"。Vec<Vec<String>>是 "String的Vec的Vec"。

你也可以對一個向量進行分片,就像在數組中一樣。

fn main() {
    let vec_of_ten = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    // Everything is the same as above except we added vec!.
    let three_to_five = &vec_of_ten[2..5];
    let start_at_two = &vec_of_ten[1..];
    let end_at_five = &vec_of_ten[..5];
    let everything = &vec_of_ten[..];

    println!("Three to five: {:?},
start at two: {:?}
end at five: {:?}
everything: {:?}", three_to_five, start_at_two, end_at_five, everything);
}

因為Vector比數組慢,我們可以用一些方法讓它更快。一個vec有一個容量,也就是給向量的空間。當你在向量上推送一個新的元素時,它會越來越接近容量。然後,如果你超過了容量,它將使其容量翻倍,並將元素複製到新的空間。這就是所謂的重新分配。我們將使用一種名為.capacity()的方法來查看向量的容量,在我們向它添加元素時。

例如,我們將使用名為.capacity()的方法來觀察一個向量的容量。

fn main() {
    let mut num_vec = Vec::new();
    println!("{}", num_vec.capacity()); // 0 elements: prints 0
    num_vec.push('a'); // add one character
    println!("{}", num_vec.capacity()); // 1 element: prints 4. Vecs with 1 item always start with capacity 4
    num_vec.push('a'); // add one more
    num_vec.push('a'); // add one more
    num_vec.push('a'); // add one more
    println!("{}", num_vec.capacity()); // 4 elements: still prints 4.
    num_vec.push('a'); // add one more
    println!("{}", num_vec.capacity()); // prints 8. We have 5 elements, but it doubled 4 to 8 to make space
}

這個打印:

0
4
4
8

所以這個向量有兩次重分配: 0到4,4到8。我們可以讓它更快:

fn main() {
    let mut num_vec = Vec::with_capacity(8); // Give it capacity 8
    num_vec.push('a'); // add one character
    println!("{}", num_vec.capacity()); // prints 8
    num_vec.push('a'); // add one more
    println!("{}", num_vec.capacity()); // prints 8
    num_vec.push('a'); // add one more
    println!("{}", num_vec.capacity()); // prints 8.
    num_vec.push('a'); // add one more
    num_vec.push('a'); // add one more // Now we have 5 elements
    println!("{}", num_vec.capacity()); // Still 8
}

這個向量有0個重分配,這是比較好的。所以如果你認為你知道你需要多少元素,你可以使用Vec::with_capacity()來使它更快。

你記得你可以用.into()&str變成String。你也可以用它把一個數組變成Vec。你必須告訴 .into() 你想要一個 Vec,但你不必選擇 Vec 的類型。如果你不想選擇,你可以寫Vec<_>

fn main() {
    let my_vec: Vec<u8> = [1, 2, 3].into();
    let my_vec2: Vec<_> = [9, 0, 10].into(); // Vec<_> means "choose the Vec type for me"
                                             // Rust will choose Vec<i32>
}

元組

Rust中的元組使用()。我們已經見過很多空元組了,因為函數中的nothing實際上意味著一個空元組。

fn do_something() {}

其實是它的簡寫:

fn do_something() -> () {}

這個函數什麼也得不到(一個空元組),也不返回什麼(一個空元組)。所以我們已經經常使用元組了。當你在一個函數中不返回任何東西時,你實際上返回的是一個空元組。

fn just_prints() {
    println!("I am printing"); // Adding ; means we return an empty tuple
}

fn main() {}

但是元組可以容納很多東西,也可以容納不同的類型。元組裡面的元素也是用數字0、1、2等來做索引的,但要訪問它們,你要用.而不是[]。讓我們把一大堆類型放到一個元組中。

fn main() {
    let random_tuple = ("Here is a name", 8, vec!['a'], 'b', [8, 9, 10], 7.7);
    println!(
        "Inside the tuple is: First item: {:?}
Second item: {:?}
Third item: {:?}
Fourth item: {:?}
Fifth item: {:?}
Sixth item: {:?}",
        random_tuple.0,
        random_tuple.1,
        random_tuple.2,
        random_tuple.3,
        random_tuple.4,
        random_tuple.5,
    )
}

這個打印:

Inside the tuple is: First item: "Here is a name"
Second item: 8
Third item: ['a']
Fourth item: 'b'
Fifth item: [8, 9, 10]
Sixth item: 7.7

這個元組的類型是 (&str, i32, Vec<char>, char, [i32; 3], f64)

你可以使用一個元組來創建多個變量。看看這段代碼。

fn main() {
    let str_vec = vec!["one", "two", "three"];
}

str_vec裡面有三個元素。如果我們想把它們拉出來呢?這時我們可以使用元組。

fn main() {
    let str_vec = vec!["one", "two", "three"];

    let (a, b, c) = (str_vec[0], str_vec[1], str_vec[2]); // call them a, b, and c
    println!("{:?}", b);
}

這就打印出"two",也就是b。這就是所謂的解構。這是因為首先變量是在結構體裡面的,但是我們又做了abc這些不是在結構體裡面的變量。

如果你需要解構,但又不想要所有的變量,你可以使用_

fn main() {
    let str_vec = vec!["one", "two", "three"];

    let (_, _, variable) = (str_vec[0], str_vec[1], str_vec[2]);
}

現在它只創建了一個叫variable的變量,但沒有為其他值做變量。

還有很多集合類型,還有很多使用數組、vec和tuple的方法。我們也將學習更多關於它們的知識,但首先我們將學習控制流。

控制流

控制流的意思是告訴你的代碼在不同的情況下該怎麼做。最簡單的控制流是if

fn main() {
    let my_number = 5;
    if my_number == 7 {
        println!("It's seven");
    }
}

另外注意,你用的是==而不是===是用來比較的,=是用來賦值的(給一個值)。另外注意,我們寫的是if my_number == 7而不是if (my_number == 7)。在Rust中,你不需要用if的括號。

else ifelse給你更多的控制:

fn main() {
    let my_number = 5;
    if my_number == 7 {
        println!("It's seven");
    } else if my_number == 6 {
        println!("It's six")
    } else {
        println!("It's a different number")
    }
}

這打印出It's a different number,因為它不等於7或6。

您可以使用 &&(和)和 ||(或)添加更多條件。

fn main() {
    let my_number = 5;
    if my_number % 2 == 1 && my_number > 0 { // % 2 means the number that remains after diving by two
        println!("It's a positive odd number");
    } else if my_number == 6 {
        println!("It's six")
    } else {
        println!("It's a different number")
    }
}

這打印出的是It's a positive odd number,因為當你把它除以2時,你有一個1的餘數,它大於0。

你可以看到,過多的ifelseelse if會很難讀。在這種情況下,你可以使用match來代替,它看起來更乾淨。但是您必須為每一個可能的結果進行匹配。例如,這將無法工作:

fn main() {
    let my_number: u8 = 5;
    match my_number {
        0 => println!("it's zero"),
        1 => println!("it's one"),
        2 => println!("it's two"),
        // ⚠️
    }
}

編譯器說:

error[E0004]: non-exhaustive patterns: `3u8..=std::u8::MAX` not covered
 --> src\main.rs:3:11
  |
3 |     match my_number {
  |           ^^^^^^^^^ pattern `3u8..=std::u8::MAX` not covered

這就意味著 "你告訴我0到2,但u8可以到255。那3呢?那4呢?5呢?" 以此類推。所以你可以加上_,意思是 "其他任何東西"。

fn main() {
    let my_number: u8 = 5;
    match my_number {
        0 => println!("it's zero"),
        1 => println!("it's one"),
        2 => println!("it's two"),
        _ => println!("It's some other number"),
    }
}

那打印It's some other number

記住匹配的規則:

  • 你寫下match,然後創建一個{}的代碼塊。
  • 在左邊寫上模式,用=>胖箭頭說明匹配時該怎麼做。
  • 每一行稱為一個 "arm"。
  • 在arm之間放一個逗號(不是分號)。

你可以用匹配來聲明一個值。

fn main() {
    let my_number = 5;
    let second_number = match my_number {
        0 => 0,
        5 => 10,
        _ => 2,
    };
}

second_number將是10。你看到最後的分號了嗎?那是因為,在match結束後,我們實際上告訴了編譯器這個信息:let second_number = 10;

你也可以在更復雜的事情上進行匹配。你用一個元組來做。

fn main() {
    let sky = "cloudy";
    let temperature = "warm";

    match (sky, temperature) {
        ("cloudy", "cold") => println!("It's dark and unpleasant today"),
        ("clear", "warm") => println!("It's a nice day"),
        ("cloudy", "warm") => println!("It's dark but not bad"),
        _ => println!("Not sure what the weather is."),
    }
}

這打印了It's dark but not bad,因為它與skytemperature的 "多雲"和 "溫暖"相匹配。

你甚至可以把if放在match裡面。這就是所謂的 "match guard"。

fn main() {
    let children = 5;
    let married = true;

    match (children, married) {
        (children, married) if married == false => println!("Not married with {} children", children),
        (children, married) if children == 0 && married == true => println!("Married but no children"),
        _ => println!("Married? {}. Number of children: {}.", married, children),
    }
}

這將打印Married? true. Number of children: 5.

在一次匹配中,你可以隨意使用 _ 。在這個關於顏色的匹配中,我們有三個顏色,但一次只能選中一個。

fn match_colours(rbg: (i32, i32, i32)) {
    match rbg {
        (r, _, _) if r < 10 => println!("Not much red"),
        (_, b, _) if b < 10 => println!("Not much blue"),
        (_, _, g) if g < 10 => println!("Not much green"),
        _ => println!("Each colour has at least 10"),
    }
}

fn main() {
    let first = (200, 0, 0);
    let second = (50, 50, 50);
    let third = (200, 50, 0);

    match_colours(first);
    match_colours(second);
    match_colours(third);

}

這個將打印:

Not much blue
Each colour has at least 10
Not much green

這也說明瞭match語句的作用,因為在第一個例子中,它只打印了Not much blue。但是first也沒有多少綠色。match語句總是在找到一個匹配項時停止,而不檢查其他的。這就是一個很好的例子,代碼編譯得很好,但不是你想要的代碼。

你可以創建一個非常大的 match 語句來解決這個問題,但是使用 for 循環可能更好。我們將很快討論循環。

匹配必須返回相同的類型。所以你不能這樣做:

fn main() {
    let my_number = 10;
    let some_variable = match my_number {
        10 => 8,
        _ => "Not ten", // ⚠️
    };
}

編譯器告訴你:

error[E0308]: `match` arms have incompatible types
  --> src\main.rs:17:14
   |
15 |       let some_variable = match my_number {
   |  _________________________-
16 | |         10 => 8,
   | |               - this is found to be of type `{integer}`
17 | |         _ => "Not ten",
   | |              ^^^^^^^^^ expected integer, found `&str`
18 | |     };
   | |_____- `match` arms have incompatible types

這樣也不行,原因同上。

fn main() {
    let some_variable = if my_number == 10 { 8 } else { "something else "}; // ⚠️
}

但是這樣就可以了,因為不是match,所以你每次都有不同的let語句。

fn main() {
    let my_number = 10;

    if my_number == 10 {
        let some_variable = 8;
    } else {
        let some_variable = "Something else";
    }
}

你也可以使用 @match 表達式的值起一個名字,然後你就可以使用它。在這個例子中,我們在一個函數中匹配一個 i32 輸入。如果是4或13,我們要在println!語句中使用這個數字。否則,我們不需要使用它。

fn match_number(input: i32) {
    match input {
    number @ 4 => println!("{} is an unlucky number in China (sounds close to 死)!", number),
    number @ 13 => println!("{} is unlucky in North America, lucky in Italy! In bocca al lupo!", number),
    _ => println!("Looks like a normal number"),
    }
}

fn main() {
    match_number(50);
    match_number(13);
    match_number(4);
}

這個打印:

Looks like a normal number
13 is unlucky in North America, lucky in Italy! In bocca al lupo!
4 is an unlucky number in China (sounds close to 死)!

結構體

有了結構體,你可以創建自己的類型。在 Rust 中,你會一直使用結構體,因為它們非常方便。結構體是用關鍵字 struct 創建的。結構體的名稱應該用UpperCamelCase(每個字用大寫字母,不要用空格)。如果你用全小寫的結構,編譯器會告訴你。

有三種類型的結構。一種是 "單元結構"。單元的意思是 "沒有任何東西"。對於一個單元結構,你只需要寫名字和一個分號。

struct FileDirectory;
fn main() {}

接下來是一個元組結構,或者說是一個未命名結構。之所以是 "未命名",是因為你只需要寫類型,而不是字段名。當你需要一個簡單的結構,並且不需要記住名字時,元組結構是很好的選擇。

struct Colour(u8, u8, u8);

fn main() {
    let my_colour = Colour(50, 0, 50); // Make a colour out of RGB (red, green, blue)
    println!("The second part of the colour is: {}", my_colour.1);
}

這時打印出The second part of the colour is: 0

第三種類型是命名結構。這可能是最常見的結構。在這個結構中,你在一個 {} 代碼塊中聲明字段名和類型。請注意,在命名結構後面不要寫分號,因為後面有一整個代碼塊。

struct Colour(u8, u8, u8); // Declare the same Colour tuple struct

struct SizeAndColour {
    size: u32,
    colour: Colour, // And we put it in our new named struct
}

fn main() {
    let my_colour = Colour(50, 0, 50);

    let size_and_colour = SizeAndColour {
        size: 150,
        colour: my_colour
    };
}

在一個命名結構中,你也可以用逗號來分隔字段。對於最後一個字段,你可以加一個逗號或不加--這取決於你。SizeAndColourcolour 後面有一個逗號。

struct Colour(u8, u8, u8); // Declare the same Colour tuple struct

struct SizeAndColour {
    size: u32,
    colour: Colour, // And we put it in our new named struct
}

fn main() {}

但你不需要它。但總是放一個逗號可能是個好主意,因為有時你會改變字段的順序。

struct Colour(u8, u8, u8); // Declare the same Colour tuple struct

struct SizeAndColour {
    size: u32,
    colour: Colour // No comma here
}

fn main() {}

然後我們決定改變順序...

struct SizeAndColour {
    colour: Colour // ⚠️ Whoops! Now this doesn't have a comma.
    size: u32,
}

fn main() {}

但無論哪種方式都不是很重要,所以你可以選擇是否使用逗號。

我們創建一個Country結構來舉例說明。Country結構有populationcapitalleader_name三個字段。

struct Country {
    population: u32,
    capital: String,
    leader_name: String
}

fn main() {
    let population = 500_000;
    let capital = String::from("Elista");
    let leader_name = String::from("Batu Khasikov");

    let kalmykia = Country {
        population: population,
        capital: capital,
        leader_name: leader_name,
    };
}

你有沒有注意到,我們把同樣的東西寫了兩次?我們寫了population: populationcapital: capitalleader_name: leader_name。實際上,你不需要這樣做:如果字段名和變量名是一樣的,你就不用寫兩次。

struct Country {
    population: u32,
    capital: String,
    leader_name: String
}

fn main() {
    let population = 500_000;
    let capital = String::from("Elista");
    let leader_name = String::from("Batu Khasikov");

    let kalmykia = Country {
        population,
        capital,
        leader_name,
    };
}

枚舉

enum是enumerations的簡稱。它們看起來與結構體非常相似,但又有所不同。這就是區別:

  • 當你想要一個東西另一個東西時,使用struct.
  • 當你想要一個東西另一個東西時,請使用 enum

所以,結構體是用於多個事物在一起,而枚舉則是用於多個選擇在一起。

要聲明一個枚舉,請寫enum,並使用一個包含選項的代碼塊,用逗號分隔。就像 struct 一樣,最後一部分可以有逗號,也可以沒有。我們將創建一個名為 ThingsInTheSky 的枚舉。

enum ThingsInTheSky {
    Sun,
    Stars,
}

fn main() {}

這是一個枚舉,因為你可以看到太陽,星星:你必須選擇一個。這些叫做變體

// create the enum with two choices
enum ThingsInTheSky {
    Sun,
    Stars,
}

// With this function we can use an i32 to create ThingsInTheSky.
fn create_skystate(time: i32) -> ThingsInTheSky {
    match time {
        6..=18 => ThingsInTheSky::Sun, // Between 6 and 18 hours we can see the sun
        _ => ThingsInTheSky::Stars, // Otherwise, we can see stars
    }
}

// With this function we can match against the two choices in ThingsInTheSky.
fn check_skystate(state: &ThingsInTheSky) {
    match state {
        ThingsInTheSky::Sun => println!("I can see the sun!"),
        ThingsInTheSky::Stars => println!("I can see the stars!")
    }
}

fn main() {
    let time = 8; // it's 8 o'clock
    let skystate = create_skystate(time); // create_skystate returns a ThingsInTheSky
    check_skystate(&skystate); // Give it a reference so it can read the variable skystate
}

這將打印出I can see the sun!

你也可以將數據添加到一個枚舉中。

enum ThingsInTheSky {
    Sun(String), // Now each variant has a string
    Stars(String),
}

fn create_skystate(time: i32) -> ThingsInTheSky {
    match time {
        6..=18 => ThingsInTheSky::Sun(String::from("I can see the sun!")), // Write the strings here
        _ => ThingsInTheSky::Stars(String::from("I can see the stars!")),
    }
}

fn check_skystate(state: &ThingsInTheSky) {
    match state {
        ThingsInTheSky::Sun(description) => println!("{}", description), // Give the string the name description so we can use it
        ThingsInTheSky::Stars(n) => println!("{}", n), // Or you can name it n. Or anything else - it doesn't matter
    }
}

fn main() {
    let time = 8; // it's 8 o'clock
    let skystate = create_skystate(time); // create_skystate returns a ThingsInTheSky
    check_skystate(&skystate); // Give it a reference so it can read the variable skystate
}

這樣打印出來的結果是一樣的:I can see the sun!

你也可以 "導入"一個枚舉,這樣你就不用打那麼多字了。下面是一個例子,我們每次在心情上匹配時都要輸入 Mood::

enum Mood {
    Happy,
    Sleepy,
    NotBad,
    Angry,
}

fn match_mood(mood: &Mood) -> i32 {
    let happiness_level = match mood {
        Mood::Happy => 10, // Here we type Mood:: every time
        Mood::Sleepy => 6,
        Mood::NotBad => 7,
        Mood::Angry => 2,
    };
    happiness_level
}

fn main() {
    let my_mood = Mood::NotBad;
    let happiness_level = match_mood(&my_mood);
    println!("Out of 1 to 10, my happiness is {}", happiness_level);
}

它打印的是Out of 1 to 10, my happiness is 7。讓我們導入,這樣我們就可以少打點字了。要導入所有的東西,寫*。注意:它和*的解引用鍵是一樣的,但完全不同。

enum Mood {
    Happy,
    Sleepy,
    NotBad,
    Angry,
}

fn match_mood(mood: &Mood) -> i32 {
    use Mood::*; // We imported everything in Mood. Now we can just write Happy, Sleepy, etc.
    let happiness_level = match mood {
        Happy => 10, // We don't have to write Mood:: anymore
        Sleepy => 6,
        NotBad => 7,
        Angry => 2,
    };
    happiness_level
}

fn main() {
    let my_mood = Mood::Happy;
    let happiness_level = match_mood(&my_mood);
    println!("Out of 1 to 10, my happiness is {}", happiness_level);
}

enum 的部分也可以變成一個整數。這是因為 Rust 給 enum 的每個arm提供了一個以 0 開頭的數字,供它自己使用。如果你的枚舉中沒有任何其他數據,你可以用它來做一些事情。

enum Season {
    Spring, // If this was Spring(String) or something it wouldn't work
    Summer,
    Autumn,
    Winter,
}

fn main() {
    use Season::*;
    let four_seasons = vec![Spring, Summer, Autumn, Winter];
    for season in four_seasons {
        println!("{}", season as u32);
    }
}

這個打印:

0
1
2
3

不過如果你想的話,你可以給它一個不同的數字--Rust並不在意,可以用同樣的方式來使用它。只需在你想要的變體上加一個 = 和你的數字。你不必給所有的都分配一個數字。但如果你不這樣做,Rust就會從前一個arm加1來賦值給當前arm。

enum Star {
    BrownDwarf = 10,
    RedDwarf = 50,
    YellowStar = 100,
    RedGiant = 1000,
    DeadStar, // Think about this one. What number will it have?
}

fn main() {
    use Star::*;
    let starvec = vec![BrownDwarf, RedDwarf, YellowStar, RedGiant];
    for star in starvec {
        match star as u32 {
            size if size <= 80 => println!("Not the biggest star."), // Remember: size doesn't mean anything. It's just a name we chose so we can print it
            size if size >= 80 => println!("This is a good-sized star."),
            _ => println!("That star is pretty big!"),
        }
    }
    println!("What about DeadStar? It's the number {}.", DeadStar as u32);
}

這個打印:

Not the biggest star.
Not the biggest star.
This is a good-sized star.
This is a good-sized star.
What about DeadStar? It's the number 1001.

DeadStar本來是4號,但現在是1001。

使用多種類型的枚舉

你知道Vec、數組等中的元素都需要相同的類型(只有tuple不同)。但其實你可以用一個枚舉來放不同的類型。想象一下,我們想有一個Vec,有u32i32。當然,你可以創建一個Vec<(u32, i32)>(一個帶有(u32, i32)元組的vec),但是我們每次只想要一個。所以這裡可以使用一個枚舉。下面是一個簡單的例子。

enum Number {
    U32(u32),
    I32(i32),
}

fn main() {}

所以有兩個變體:U32變體裡面有u32I32變體裡面有i32U32I32只是我們起的名字。它們可能是UThirtyTwoIThirtyTwo或其他任何東西。

現在,如果我們把它們放到 Vec 中,我們就會有一個 Vec<Number>,編譯器很高興,因為都是同一個類型。編譯器並不在乎我們有 u32i32,因為它們都在一個叫做 Number 的單一類型裡面。因為它是一個枚舉,你必須選擇一個,這就是我們想要的。我們將使用.is_positive()方法來挑選。如果是 true,那麼我們將選擇 U32,如果是 false,那麼我們將選擇 I32

現在的代碼是這樣的。

enum Number {
    U32(u32),
    I32(i32),
}

fn get_number(input: i32) -> Number {
    let number = match input.is_positive() {
        true => Number::U32(input as u32), // change it to u32 if it's positive
        false => Number::I32(input), // otherwise just give the number because it's already i32
    };
    number
}


fn main() {
    let my_vec = vec![get_number(-800), get_number(8)];

    for item in my_vec {
        match item {
            Number::U32(number) => println!("It's a u32 with the value {}", number),
            Number::I32(number) => println!("It's an i32 with the value {}", number),
        }
    }
}

這就打印出了我們想看到的東西。

It's an i32 with the value -800
It's a u32 with the value 8

循環

有了循環,你可以告訴 Rust 繼續某事,直到你想讓它停止。您使用 loop 來啟動一個不會停止的循環,除非您告訴它何時break

fn main() { // This program will never stop
    loop {

    }
}

所以,我們要告訴編譯器什麼時候能停止:

fn main() {
    let mut counter = 0; // set a counter to 0
    loop {
        counter +=1; // increase the counter by 1
        println!("The counter is now: {}", counter);
        if counter == 5 { // stop when counter == 5
            break;
        }
    }
}

這將打印:

The counter is now: 1
The counter is now: 2
The counter is now: 3
The counter is now: 4
The counter is now: 5

如果你在一個循環裡面有一個循環,你可以給它們命名。有了名字,你可以告訴 Rust 要從哪個循環中 break 出來。使用 ' (稱為 "tick") 和 : 來給它命名。

fn main() {
    let mut counter = 0;
    let mut counter2 = 0;
    println!("Now entering the first loop.");

    'first_loop: loop {
        // Give the first loop a name
        counter += 1;
        println!("The counter is now: {}", counter);
        if counter > 9 {
            // Starts a second loop inside this loop
            println!("Now entering the second loop.");

            'second_loop: loop {
                // now we are inside 'second_loop
                println!("The second counter is now: {}", counter2);
                counter2 += 1;
                if counter2 == 3 {
                    break 'first_loop; // Break out of 'first_loop so we can exit the program
                }
            }
        }
    }
}

這將打印:

Now entering the first loop.
The counter is now: 1
The counter is now: 2
The counter is now: 3
The counter is now: 4
The counter is now: 5
The counter is now: 6
The counter is now: 7
The counter is now: 8
The counter is now: 9
The counter is now: 10
Now entering the second loop.
The second counter is now: 0
The second counter is now: 1
The second counter is now: 2

while循環是指在某件事情還在true時繼續的循環。每一次循環,Rust 都會檢查它是否仍然是 true。如果變成false,Rust會停止循環。

fn main() {
    let mut counter = 0;

    while counter < 5 {
        counter +=1;
        println!("The counter is now: {}", counter);
    }
}

for循環可以讓你告訴Rust每次要做什麼。但是在 for 循環中,循環會在一定次數後停止。for循環經常使用範圍。你使用 ....= 來創建一個範圍。

  • ..創建一個排他的範圍:0..3創建了0, 1, 2.
  • ..=創建一個包含的範圍: 0..=3創建0, 1, 20..=3 = 0, 1, 2, 3.
fn main() {
    for number in 0..3 {
        println!("The number is: {}", number);
    }

    for number in 0..=3 {
        println!("The next number is: {}", number);
    }
}

這個將打印:

The number is: 0
The number is: 1
The number is: 2
The next number is: 0
The next number is: 1
The next number is: 2
The next number is: 3

同時注意到,number成為0..3的變量名。我們可以把它叫做 n,或者 ntod_het___hno_f,或者任何名字。然後,我們可以在println!中使用這個名字。

如果你不需要變量名,就用_

fn main() {
    for _ in 0..3 {
        println!("Printing the same thing three times");
    }
}

這個打印:

Printing the same thing three times
Printing the same thing three times
Printing the same thing three times

因為我們每次都沒有給它任何數字來打印。

而實際上,如果你給了一個變量名卻不用,Rust會告訴你:

fn main() {
    for number in 0..3 {
        println!("Printing the same thing three times");
    }
}

這打印的內容和上面一樣。程序編譯正常,但Rust會提醒你沒有使用number:

warning: unused variable: `number`
 --> src\main.rs:2:9
  |
2 |     for number in 0..3 {
  |         ^^^^^^ help: if this is intentional, prefix it with an underscore: `_number`

Rust 建議寫 _number 而不是 _。在變量名前加上 _ 意味著 "也許我以後會用到它"。但是隻用_意味著 "我根本不關心這個變量"。所以,如果你以後會使用它們,並且不想讓編譯器告訴你,你可以在變量名前面加上_

你也可以用break來返回一個值。 你把值寫在 break 之後,並使用 ;。下面是一個用 loop 和一個斷點給出 my_number 值的例子。

fn main() {
    let mut counter = 5;
    let my_number = loop {
        counter +=1;
        if counter % 53 == 3 {
            break counter;
        }
    };
    println!("{}", my_number);
}

這時打印出56break counter;的意思是 "中斷並返回計數器的值"。而且因為整個塊以let開始,所以my_number得到值。

現在我們知道了如何使用循環,這裡有一個更好的解決方案來解決我們之前的顏色 "匹配"問題。這是一個更好的解決方案,因為我們要比較所有的東西,而 "for"循環會查看每一項。

fn match_colours(rbg: (i32, i32, i32)) {
    println!("Comparing a colour with {} red, {} blue, and {} green:", rbg.0, rbg.1, rbg.2);
    let new_vec = vec![(rbg.0, "red"), (rbg.1, "blue"), (rbg.2, "green")]; // Put the colours in a vec. Inside are tuples with the colour names
    let mut all_have_at_least_10 = true; // Start with true. We will set it to false if one colour is less than 10
    for item in new_vec {
        if item.0 < 10 {
            all_have_at_least_10 = false; // Now it's false
            println!("Not much {}.", item.1) // And we print the colour name.
        }
    }
    if all_have_at_least_10 { // Check if it's still true, and print if true
        println!("Each colour has at least 10.")
    }
    println!(); // Add one more line
}

fn main() {
    let first = (200, 0, 0);
    let second = (50, 50, 50);
    let third = (200, 50, 0);

    match_colours(first);
    match_colours(second);
    match_colours(third);
}

這個打印:

Comparing a colour with 200 red, 0 blue, and 0 green:
Not much blue.
Not much green.

Comparing a colour with 50 red, 50 blue, and 50 green:
Each colour has at least 10.

Comparing a colour with 200 red, 50 blue, and 0 green:
Not much green.

實現結構體和枚舉

在這裡你可以開始賦予你的結構體和枚舉一些真正的力量。要調用 structenum 上的函數,請使用 impl 塊。這些函數被稱為方法impl塊中有兩種方法。

  • 方法:這些方法取self(或 &self&mut self )。常規方法使用"."(一個句號)。.clone()是一個常規方法的例子。
  • 關聯函數(在某些語言中被稱為 "靜態 "方法):這些函數不使用self。關聯的意思是 "與之相關"。它們的書寫方式不同,使用::String::from()是一個關聯函數,Vec::new()也是。你看到的關聯函數最常被用來創建新的變量。

在我們的例子中,我們將創建Animal並打印它們。

對於新的structenum,如果你想使用{:?}來打印,你需要給它Debug,所以我們將這樣做:如果你在結構體或枚舉上面寫了#[derive(Debug)],那麼你就可以用{:?}來打印。這些帶有#[]的信息被稱為屬性。你有時可以用它們來告訴編譯器給你的結構體一個能力,比如Debug。屬性有很多,我們以後會學習它們。但是derive可能是最常見的,你經常在結構體和枚舉上面看到它。

#[derive(Debug)]
struct Animal {
    age: u8,
    animal_type: AnimalType,
}

#[derive(Debug)]
enum AnimalType {
    Cat,
    Dog,
}

impl Animal {
    fn new() -> Self {
        // Self means Animal.
        //You can also write Animal instead of Self

        Self {
            // When we write Animal::new(), we always get a cat that is 10 years old
            age: 10,
            animal_type: AnimalType::Cat,
        }
    }

    fn change_to_dog(&mut self) { // because we are inside Animal, &mut self means &mut Animal
                                  // use .change_to_dog() to change the cat to a dog
                                  // with &mut self we can change it
        println!("Changing animal to dog!");
        self.animal_type = AnimalType::Dog;
    }

    fn change_to_cat(&mut self) {
        // use .change_to_cat() to change the dog to a cat
        // with &mut self we can change it
        println!("Changing animal to cat!");
        self.animal_type = AnimalType::Cat;
    }

    fn check_type(&self) {
        // we want to read self
        match self.animal_type {
            AnimalType::Dog => println!("The animal is a dog"),
            AnimalType::Cat => println!("The animal is a cat"),
        }
    }
}



fn main() {
    let mut new_animal = Animal::new(); // Associated function to create a new animal
                                        // It is a cat, 10 years old
    new_animal.check_type();
    new_animal.change_to_dog();
    new_animal.check_type();
    new_animal.change_to_cat();
    new_animal.check_type();
}

這個打印:

The animal is a cat
Changing animal to dog!
The animal is a dog
Changing animal to cat!
The animal is a cat

記住,Self(類型Self)和self(變量self)是縮寫。(縮寫=簡寫方式)

所以,在我們的代碼中,Self = Animal。另外,fn change_to_dog(&mut self)的意思是fn change_to_dog(&mut Animal)

下面再舉一個小例子。這次我們將在enum上使用impl

enum Mood {
    Good,
    Bad,
    Sleepy,
}

impl Mood {
    fn check(&self) {
        match self {
            Mood::Good => println!("Feeling good!"),
            Mood::Bad => println!("Eh, not feeling so good"),
            Mood::Sleepy => println!("Need sleep NOW"),
        }
    }
}

fn main() {
    let my_mood = Mood::Sleepy;
    my_mood.check();
}

打印出Need sleep NOW

解構

我們再來看一些解構。你可以通過使用let倒過來從一個結構體或枚舉中獲取值。我們瞭解到這是destructuring,因為你得到的變量不是結構體的一部分。現在你分別得到了它們的值。首先是一個簡單的例子。

struct Person { // make a simple struct for a person
    name: String,
    real_name: String,
    height: u8,
    happiness: bool
}

fn main() {
    let papa_doc = Person { // create variable papa_doc
        name: "Papa Doc".to_string(),
        real_name: "Clarence".to_string(),
        height: 170,
        happiness: false
    };

    let Person { // destructure papa_doc
        name: a,
        real_name: b,
        height: c,
        happiness: d
    } = papa_doc;

    println!("They call him {} but his real name is {}. He is {} cm tall and is he happy? {}", a, b, c, d);
}

這個打印:They call him Papa Doc but his real name is Clarence. He is 170 cm tall and is he happy? false

你可以看到,這是倒過來的。首先我們說let papa_doc = Person { fields }來創建結構。然後我們說 let Person { fields } = papa_doc 來解構它。

你不必寫name: a--你可以直接寫name。但這裡我們寫 name: a 是因為我們想使用一個名字為 a 的變量。

現在再舉一個更大的例子。在這個例子中,我們有一個 City 結構。我們給它一個new函數來創建它。然後我們有一個 process_city_values 函數來處理這些值。在函數中,我們只是創建了一個 Vec,但你可以想象,我們可以在解構它之後做更多的事情。

struct City {
    name: String,
    name_before: String,
    population: u32,
    date_founded: u32,
}

impl City {
    fn new(name: String, name_before: String, population: u32, date_founded: u32) -> Self {
        Self {
            name,
            name_before,
            population,
            date_founded,
        }
    }
}

fn process_city_values(city: &City) {
    let City {
        name,
        name_before,
        population,
        date_founded,
    } = city;
        // now we have the values to use separately
    let two_names = vec![name, name_before];
    println!("The city's two names are {:?}", two_names);
}

fn main() {
    let tallinn = City::new("Tallinn".to_string(), "Reval".to_string(), 426_538, 1219);
    process_city_values(&tallinn);
}

這將打印出The city's two names are ["Tallinn", "Reval"]

引用和點運算符

我們瞭解到,當你有一個引用時,你需要使用*來獲取值。引用是一種不同的類型,所以這是無法運行的:

fn main() {
    let my_number = 9;
    let reference = &my_number;

    println!("{}", my_number == reference); // ⚠️
}

編譯器打印。

error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src\main.rs:5:30
  |
5 |     println!("{}", my_number == reference);
  |                              ^^ no implementation for `{integer} == &{integer}`

所以我們把第5行改成println!("{}", my_number == *reference);,現在打印的是true,因為現在是i32 == i32,而不是i32 == &i32。這就是所謂的解引用。

但是當你使用一個方法時,Rust會為你解除引用。方法中的 . 被稱為點運算符,它可以免費進行遞歸。

首先,讓我們創建一個有一個 u8 字段的結構。然後,我們將對它進行引用,並嘗試進行比較。它將無法工作。

struct Item {
    number: u8,
}

fn main() {
    let item = Item {
        number: 8,
    };

    let reference_number = &item.number; // reference number type is &u8

    println!("{}", reference_number == 8); // ⚠️ &u8 and u8 cannot be compared
}

為了讓它工作,我們需要取消定義。println!("{}", *reference_number == 8);.

但如果使用點運算符,我們不需要*。例如

struct Item {
    number: u8,
}

fn main() {
    let item = Item {
        number: 8,
    };

    let reference_item = &item;

    println!("{}", reference_item.number == 8); // we don't need to write *reference_item.number
}

現在讓我們為 Item 創建一個方法,將 number 與另一個數字進行比較。我們不需要在任何地方使用 *

struct Item {
    number: u8,
}

impl Item {
    fn compare_number(&self, other_number: u8) { // takes a reference to self
        println!("Are {} and {} equal? {}", self.number, other_number, self.number == other_number);
            // We don't need to write *self.number
    }
}

fn main() {
    let item = Item {
        number: 8,
    };

    let reference_item = &item; // This is type &Item
    let reference_item_two = &reference_item; // This is type &&Item

    item.compare_number(8); // the method works
    reference_item.compare_number(8); // it works here too
    reference_item_two.compare_number(8); // and here

}

所以只要記住:當你使用.運算符時,你不需要擔心*

泛型

在函數中,你要寫出採取什麼類型作為輸入。

fn return_number(number: i32) -> i32 {
    println!("Here is your number.");
    number
}

fn main() {
    let number = return_number(5);
}

但是如果你想用的不僅僅是i32呢?你可以用泛型來解決。Generics的意思是 "也許是一種類型,也許是另一種類型"。

對於泛型,你可以使用角括號,裡面加上類型,像這樣。<T> 這意味著 "任何類型你都可以放入函數中" 通常情況下,generics使用一個大寫字母的類型(T、U、V等),儘管你不必只使用一個字母。

這就是你如何改變函數使其通用的方法。

fn return_number<T>(number: T) -> T {
    println!("Here is your number.");
    number
}

fn main() {
    let number = return_number(5);
}

重要的部分是函數名後的<T>。如果沒有這個,Rust會認為T是一個具體的(具體的=不是通用的)類型。 如Stringi8

如果我們寫出一個類型名,這就更容易理解了。看看我們把 T 改成 MyType 會發生什麼。

#![allow(unused)]
fn main() {
fn return_number(number: MyType) -> MyType { // ⚠️
    println!("Here is your number.");
    number
}
}

大家可以看到,MyType是具體的,不是通用的。所以我們需要寫這個,所以現在就可以了。

fn return_number<MyType>(number: MyType) -> MyType {
    println!("Here is your number.");
    number
}

fn main() {
    let number = return_number(5);
}

所以單字母T是人的眼睛,但函數名後面的部分是編譯器的 "眼睛"。沒有了它,就不通用了。

現在我們再回到類型T,因為Rust代碼通常使用T

你會記得Rust中有些類型是Copy,有些是Clone,有些是Display,有些是Debug,等等。用Debug,我們可以用{:?}來打印。所以現在大家可以看到,我們如果要打印T就有問題了。

fn print_number<T>(number: T) {
    println!("Here is your number: {:?}", number); // ⚠️
}

fn main() {
    print_number(5);
}

print_number需要Debug打印number,但是TDebug是一個類型嗎?也許不是。也許它沒有#[derive(Debug)],誰知道呢?編譯器也不知道,所以它給出了一個錯誤。

error[E0277]: `T` doesn't implement `std::fmt::Debug`
  --> src\main.rs:29:43
   |
29 |     println!("Here is your number: {:?}", number);
   |                                           ^^^^^^ `T` cannot be formatted using `{:?}` because it doesn't implement `std::fmt::Debug`

T沒有實現Debug。那麼我們是否要為T實現Debug呢?不,因為我們不知道T是什麼。但是我們可以告訴函數。"別擔心,因為任何T類型的函數都會有Debug"

use std::fmt::Debug; // Debug is located at std::fmt::Debug. So now we can just write 'Debug'.

fn print_number<T: Debug>(number: T) { // <T: Debug> is the important part
    println!("Here is your number: {:?}", number);
}

fn main() {
    print_number(5);
}

所以現在編譯器知道:"好的,這個類型T要有Debug"。現在代碼工作了,因為i32有Debug。現在我們可以給它很多類型。String, &str, 等等,因為它們都有Debug.

現在我們可以創建一個結構,並用#[derive(Debug)]給它Debug,所以現在我們也可以打印它。我們的函數可以取i32,Animal結構等。

use std::fmt::Debug;

#[derive(Debug)]
struct Animal {
    name: String,
    age: u8,
}

fn print_item<T: Debug>(item: T) {
    println!("Here is your item: {:?}", item);
}

fn main() {
    let charlie = Animal {
        name: "Charlie".to_string(),
        age: 1,
    };

    let number = 55;

    print_item(charlie);
    print_item(number);
}

這個打印:

Here is your item: Animal { name: "Charlie", age: 1 }
Here is your item: 55

有時候,我們在一個通用函數中需要不止一個類型。我們必須寫出每個類型的名稱,並考慮如何使用它。在這個例子中,我們想要兩個類型。首先我們要打印一個類型為T的語句。用{}打印比較好,所以我們會要求用Display來打印T

其次是類型U,num_1num_2這兩個變量的類型為U(U是某種數字)。我們想要比較它們,所以我們需要PartialOrd。這個特性讓我們可以使用<>==等。我們也想打印它們,所以我們也需要Display來打印U

use std::fmt::Display;
use std::cmp::PartialOrd;

fn compare_and_display<T: Display, U: Display + PartialOrd>(statement: T, num_1: U, num_2: U) {
    println!("{}! Is {} greater than {}? {}", statement, num_1, num_2, num_1 > num_2);
}

fn main() {
    compare_and_display("Listen up!", 9, 8);
}

這就打印出了Listen up!! Is 9 greater than 8? true

所以fn compare_and_display<T: Display, U: Display + PartialOrd>(statement: T, num_1: U, num_2: U)說。

  • 函數名稱是compare_and_display,
  • 第一個類型是T,它是通用的。它必須是一個可以用{}打印的類型。
  • 下一個類型是U,它是通用的。它必須是一個可以用{}打印的類型。另外,它必須是一個可以比較的類型(使用 ><==)。

現在我們可以給compare_and_display不同的類型。statement可以是一個String,一個&str,任何有Display的類型。

為了讓通用函數更容易讀懂,我們也可以這樣寫,在代碼塊之前就寫上where

use std::cmp::PartialOrd;
use std::fmt::Display;

fn compare_and_display<T, U>(statement: T, num_1: U, num_2: U)
where
    T: Display,
    U: Display + PartialOrd,
{
    println!("{}! Is {} greater than {}? {}", statement, num_1, num_2, num_1 > num_2);
}

fn main() {
    compare_and_display("Listen up!", 9, 8);
}

當你有很多通用類型時,使用where是一個好主意。

還要注意。

  • 如果你有一個類型T和另一個類型T,它們必須是相同的。
  • 如果你有一個類型T和另一個類型U,它們可以是不同的。但它們也可以是相同的。

比如說

use std::fmt::Display;

fn say_two<T: Display, U: Display>(statement_1: T, statement_2: U) { // Type T needs Display, type U needs Display
    println!("I have two things to say: {} and {}", statement_1, statement_2);
}

fn main() {

    say_two("Hello there!", String::from("I hate sand.")); // Type T is a &str, but type U is a String.
    say_two(String::from("Where is Padme?"), String::from("Is she all right?")); // Both types are String.
}

這個打印:

I have two things to say: Hello there! and I hate sand.
I have two things to say: Where is Padme? and Is she all right?

Option和Result

我們現在理解了枚舉和泛型,所以我們可以理解OptionResult。Rust使用這兩個枚舉來使代碼更安全。

我們將從Option開始。

Option

當你有一個可能存在,也可能不存在的值時,你就用Option。當一個值存在的時候就是Some(value),不存在的時候就是None,下面是一個壞代碼的例子,可以用Option來改進。

    // ⚠️
fn take_fifth(value: Vec<i32>) -> i32 {
    value[4]
}

fn main() {
    let new_vec = vec![1, 2];
    let index = take_fifth(new_vec);
}

當我們運行這段代碼時,它崩潰。以下是信息。

thread 'main' panicked at 'index out of bounds: the len is 2 but the index is 4', src\main.rs:34:5

崩潰的意思是,程序在問題發生之前就停止了。Rust看到函數想要做一些不可能的事情,就會停止。它 "解開堆棧"(從堆棧中取值),並告訴你 "對不起,我不能這樣做"。

所以現在我們將返回類型從i32改為Option<i32>。這意味著 "如果有的話給我一個Some(i32),如果沒有的話給我一個None"。我們說i32是 "包"在一個Option裡面,也就是說它在一個Option裡面。你必須做一些事情才能把這個值弄出來。

fn take_fifth(value: Vec<i32>) -> Option<i32> {
    if value.len() < 5 { // .len() gives the length of the vec.
                         // It must be at least 5.
        None
    } else {
        Some(value[4])
    }
}

fn main() {
    let new_vec = vec![1, 2];
    let bigger_vec = vec![1, 2, 3, 4, 5];
    println!("{:?}, {:?}", take_fifth(new_vec), take_fifth(bigger_vec));
}

這個打印的是None, Some(5)。這下好了,因為現在我們再也不崩潰了。但是我們如何得到5的值呢?

我們可以用 .unwrap() 在一個Option中獲取值,但要小心 .unwrap()。這就像拆禮物一樣:也許裡面有好東西,也許裡面有一條憤怒的蛇。只有在你確定的情況下,你才會想要.unwrap()。如果你拆開一個None的值,程序就會崩潰。

// ⚠️
fn take_fifth(value: Vec<i32>) -> Option<i32> {
    if value.len() < 5 {
        None
    } else {
        Some(value[4])
    }
}

fn main() {
    let new_vec = vec![1, 2];
    let bigger_vec = vec![1, 2, 3, 4, 5];
    println!("{:?}, {:?}",
        take_fifth(new_vec).unwrap(), // this one is None. .unwrap() will panic!
        take_fifth(bigger_vec).unwrap()
    );
}

消息是:

thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src\main.rs:14:9

但我們不需要使用.unwrap()。我們可以使用match。那麼我們就可以把我們有Some的值打印出來,如果有None的值就不要碰。比如說

fn take_fifth(value: Vec<i32>) -> Option<i32> {
    if value.len() < 5 {
        None
    } else {
        Some(value[4])
    }
}

fn handle_option(my_option: Vec<Option<i32>>) {
  for item in my_option {
    match item {
      Some(number) => println!("Found a {}!", number),
      None => println!("Found a None!"),
    }
  }
}

fn main() {
    let new_vec = vec![1, 2];
    let bigger_vec = vec![1, 2, 3, 4, 5];
    let mut option_vec = Vec::new(); // Make a new vec to hold our options
                                     // The vec is type: Vec<Option<i32>>. That means a vec of Option<i32>.

    option_vec.push(take_fifth(new_vec)); // This pushes "None" into the vec
    option_vec.push(take_fifth(bigger_vec)); // This pushes "Some(5)" into the vec

    handle_option(option_vec); // handle_option looks at every option in the vec.
                               // It prints the value if it is Some. It doesn't touch it if it is None.
}

這個打印:

Found a None!
Found a 5!

因為我們知道泛型,所以我們能夠讀懂Option的代碼。它看起來是這樣的:

enum Option<T> {
    None,
    Some(T),
}

fn main() {}

要記住的重要一點是:有了Some,你就有了一個類型為T的值(任何類型)。還要注意的是,enum名字後面的角括號圍繞著T是告訴編譯器它是通用的。它沒有Display這樣的trait或任何東西來限制它,所以它可以是任何東西。但是對於None,你什麼都沒有。

所以在match語句中,對於Option,你不能說。

#![allow(unused)]
fn main() {
// 🚧
Some(value) => println!("The value is {}", value),
None(value) => println!("The value is {}", value),
}

因為None只是None

當然,還有更簡單的方法來使用Option。在這段代碼中,我們將使用一個叫做 .is_some() 的方法來告訴我們是否是 Some。(是的,還有一個叫做.is_none()的方法。)在這個更簡單的方法中,我們不需要handle_option()了。我們也不需要Option的vec了。

fn take_fifth(value: Vec<i32>) -> Option<i32> {
    if value.len() < 5 {
        None
    } else {
        Some(value[4])
    }
}

fn main() {
    let new_vec = vec![1, 2];
    let bigger_vec = vec![1, 2, 3, 4, 5];
    let vec_of_vecs = vec![new_vec, bigger_vec];
    for vec in vec_of_vecs {
        let inside_number = take_fifth(vec);
        if inside_number.is_some() {
            // .is_some() returns true if we get Some, false if we get None
            println!("We got: {}", inside_number.unwrap()); // now it is safe to use .unwrap() because we already checked
        } else {
            println!("We got nothing.");
        }
    }
}

這個將打印:

We got nothing.
We got: 5

Result

Result和Option類似,但這裡的區別是。

  • Option大約是SomeNone(有值或無值)。
  • Result大約是OkErr(還好的結果,或錯誤的結果)。

所以,Option是如果你在想:"也許會有,也許不會有。"也許會有一些東西,也許不會有。" 但Result是如果你在想: "也許會失敗"

比較一下,這裡是Option和Result的簽名。

enum Option<T> {
    None,
    Some(T),
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

fn main() {}

所以Result在 "Ok "裡面有一個值,在 "Err "裡面有一個值。這是因為錯誤通常包含描述錯誤的信息。

Result<T, E>的意思是你要想好Ok要返回什麼,Err要返回什麼。其實,你可以決定任何事情。甚至這個也可以。

fn check_error() -> Result<(), ()> {
    Ok(())
}

fn main() {
    check_error();
}

check_error說 "如果得到Ok就返回(),如果得到Err就返回()"。然後我們用()返回Ok

編譯器給了我們一個有趣的警告。

warning: unused `std::result::Result` that must be used
 --> src\main.rs:6:5
  |
6 |     check_error();
  |     ^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: this `Result` may be an `Err` variant, which should be handled

這是真的:我們只返回了Result,但它可能是一個Err。所以讓我們稍微處理一下這個錯誤,儘管我們仍然沒有真正做任何事情。

fn give_result(input: i32) -> Result<(), ()> {
    if input % 2 == 0 {
        return Ok(())
    } else {
        return Err(())
    }
}

fn main() {
    if give_result(5).is_ok() {
        println!("It's okay, guys")
    } else {
        println!("It's an error, guys")
    }
}

打印出It's an error, guys。所以我們只是處理了第一個錯誤。

記住,輕鬆檢查的四種方法是.is_some()is_none()is_ok()is_err()

有時,一個帶有Result的函數會用String來表示Err的值。這不是最好的方法,但比我們目前所做的要好一些。

fn check_if_five(number: i32) -> Result<i32, String> {
    match number {
        5 => Ok(number),
        _ => Err("Sorry, the number wasn't five.".to_string()), // This is our error message
    }
}

fn main() {
    let mut result_vec = Vec::new(); // Create a new vec for the results

    for number in 2..7 {
        result_vec.push(check_if_five(number)); // push each result into the vec
    }

    println!("{:?}", result_vec);
}

我們的Vec打印:

[Err("Sorry, the number wasn\'t five."), Err("Sorry, the number wasn\'t five."), Err("Sorry, the number wasn\'t five."), Ok(5),
Err("Sorry, the number wasn\'t five.")]

就像Option一樣,在Err上用.unwrap()就會崩潰。

    // ⚠️
fn main() {
    let error_value: Result<i32, &str> = Err("There was an error"); // Create a Result that is already an Err
    println!("{}", error_value.unwrap()); // Unwrap it
}

程序崩潰,打印。

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "There was an error"', src\main.rs:30:20

這些信息可以幫助你修正你的代碼。src\main.rs:30:20的意思是 "在目錄src的main.rs內,第30行和第20列"。所以你可以去那裡查看你的代碼並修復問題。

你也可以創建自己的錯誤類型,標準庫中的Result函數和其他人的代碼通常都會這樣做。例如,標準庫中的這個函數。

#![allow(unused)]
fn main() {
// 🚧
pub fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error>
}

這個函數接收一個字節向量(u8),並嘗試創建一個String,所以Result的成功情況是String,錯誤情況是FromUtf8Error。你可以給你的錯誤類型起任何你想要的名字。

使用 matchOptionResult 有時需要很多代碼。例如,.get() 方法在 Vec 上返回 Option

fn main() {
    let my_vec = vec![2, 3, 4];
    let get_one = my_vec.get(0); // 0 to get the first number
    let get_two = my_vec.get(10); // Returns None
    println!("{:?}", get_one);
    println!("{:?}", get_two);
}

此打印

Some(2)
None

所以現在我們可以匹配得到數值。讓我們使用0到10的範圍,看看是否符合my_vec中的數字。

fn main() {
    let my_vec = vec![2, 3, 4];

    for index in 0..10 {
      match my_vec.get(index) {
        Some(number) => println!("The number is: {}", number),
        None => {}
      }
    }
}

這是好的,但是我們對None不做任何處理,因為我們不關心。這裡我們可以用if let把代碼變小。if let的意思是 "符合就做,不符合就不做"。if let是在你不要求對所有的東西都匹配的時候使用。

fn main() {
    let my_vec = vec![2, 3, 4];

    for index in 0..10 {
      if let Some(number) = my_vec.get(index) {
        println!("The number is: {}", number);
      }
    }
}

重要的是要記住if let Some(number) = my_vec.get(index)的意思是 "如果你從my_vec.get(index)得到Some(number)"。

另外注意:它使用的是一個=。它不是一個布爾值。

while let就像if let的一個while循環。想象一下,我們有這樣的氣象站數據。

["Berlin", "cloudy", "5", "-7", "78"]
["Athens", "sunny", "not humid", "20", "10", "50"]

我們想得到數字,但不想得到文字。對於數字,我們可以使用一個叫做 parse::<i32>() 的方法。parse()是方法,::<i32>是類型。它將嘗試把 &str 變成 i32,如果可以的話就把它給我們。它返回一個 Result,因為它可能無法工作(比如你想讓它解析 "Billybrobby"--那不是一個數字)。

我們還將使用 .pop()。這將從向量中取出最後一項。

fn main() {
    let weather_vec = vec![
        vec!["Berlin", "cloudy", "5", "-7", "78"],
        vec!["Athens", "sunny", "not humid", "20", "10", "50"],
    ];
    for mut city in weather_vec {
        println!("For the city of {}:", city[0]); // In our data, every first item is the city name
        while let Some(information) = city.pop() {
            // This means: keep going until you can't pop anymore
            // When the vector reaches 0 items, it will return None
            // and it will stop.
            if let Ok(number) = information.parse::<i32>() {
                // Try to parse the variable we called information
                // This returns a result. If it's Ok(number), it will print it
                println!("The number is: {}", number);
            }  // We don't write anything here because we do nothing if we get an error. Throw them all away
        }
    }
}

這將打印:

For the city of Berlin:
The number is: 78
The number is: -7
The number is: 5
For the city of Athens:
The number is: 50
The number is: 10
The number is: 20

其他集合類型

Rust還有很多集合類型。你可以在標準庫中的 https://doc.rust-lang.org/beta/std/collections/ 看到它們。那個頁面對為什麼要使用一種類型有很好的解釋,所以如果你不知道你想要什麼類型,就去那裡。這些集合都在標準庫的std::collections裡面。使用它們的最好方法是使用 use 語句。 就像我們的enums一樣。我們將從HashMap開始,這是很常見的。

HashMap和BTreeMap

HashMap是由keysvalues組成的集合。你使用鍵來查找與鍵匹配的值。你可以只用HashMap::new()創建一個新的HashMap,並使用.insert(key, value)來插入元素。

HashMap是沒有順序的,所以如果你把HashMap中的每一個鍵都打印在一起,可能會打印出不同的結果。我們可以在一個例子中看到這一點。

use std::collections::HashMap; // This is so we can just write HashMap instead of std::collections::HashMap every time

struct City {
    name: String,
    population: HashMap<u32, u32>, // This will have the year and the population for the year
}

fn main() {

    let mut tallinn = City {
        name: "Tallinn".to_string(),
        population: HashMap::new(), // So far the HashMap is empty
    };

    tallinn.population.insert(1372, 3_250); // insert three dates
    tallinn.population.insert(1851, 24_000);
    tallinn.population.insert(2020, 437_619);


    for (year, population) in tallinn.population { // The HashMap is HashMap<u32, u32> so it returns a two items each time
        println!("In the year {} the city of {} had a population of {}.", year, tallinn.name, population);
    }
}

這個打印:

In the year 1372 the city of Tallinn had a population of 3250.
In the year 2020 the city of Tallinn had a population of 437619.
In the year 1851 the city of Tallinn had a population of 24000.

或者可能會打印。

In the year 1851 the city of Tallinn had a population of 24000.
In the year 2020 the city of Tallinn had a population of 437619.
In the year 1372 the city of Tallinn had a population of 3250.

你可以看到,它不按順序排列。

如果你想要一個可以排序的HashMap,你可以用BTreeMap。其實它們之間是非常相似的,所以我們可以快速的把我們的HashMap改成BTreeMap來看看。大家可以看到,這幾乎是一樣的代碼。

use std::collections::BTreeMap; // Just change HashMap to BTreeMap

struct City {
    name: String,
    population: BTreeMap<u32, u32>, // Just change HashMap to BTreeMap
}

fn main() {

    let mut tallinn = City {
        name: "Tallinn".to_string(),
        population: BTreeMap::new(), // Just change HashMap to BTreeMap
    };

    tallinn.population.insert(1372, 3_250);
    tallinn.population.insert(1851, 24_000);
    tallinn.population.insert(2020, 437_619);

    for (year, population) in tallinn.population {
        println!("In the year {} the city of {} had a population of {}.", year, tallinn.name, population);
    }
}

現在會一直打印。

In the year 1372 the city of Tallinn had a population of 3250.
In the year 1851 the city of Tallinn had a population of 24000.
In the year 2020 the city of Tallinn had a population of 437619.

現在我們再來看看HashMap

只要把鍵放在[]的方括號裡,就可以得到HashMap的值。在接下來的這個例子中,我們將帶出Bielefeld這個鍵的值,也就是Germany。但是要注意,因為如果沒有鍵,程序會崩潰。比如你寫了println!("{:?}", city_hashmap["Bielefeldd"]);,那麼就會崩潰,因為Bielefeldd不存在。

如果你不確定會有一個鍵,你可以使用.get(),它返回一個Option。如果它存在,將是Some(value),如果不存在,你將得到None,而不是使程序崩潰。這就是為什麼 .get() 是從 HashMap 中獲取一個值的比較安全的方法。

use std::collections::HashMap;

fn main() {
    let canadian_cities = vec!["Calgary", "Vancouver", "Gimli"];
    let german_cities = vec!["Karlsruhe", "Bad Doberan", "Bielefeld"];

    let mut city_hashmap = HashMap::new();

    for city in canadian_cities {
        city_hashmap.insert(city, "Canada");
    }
    for city in german_cities {
        city_hashmap.insert(city, "Germany");
    }

    println!("{:?}", city_hashmap["Bielefeld"]);
    println!("{:?}", city_hashmap.get("Bielefeld"));
    println!("{:?}", city_hashmap.get("Bielefeldd"));
}

這個打印:

"Germany"
Some("Germany")
None

這是因為Bielefeld存在,但Bielefeldd不存在。

如果HashMap已經有一個鍵,當你試圖把它放進去時,它將覆蓋它的值。

use std::collections::HashMap;

fn main() {
    let mut book_hashmap = HashMap::new();

    book_hashmap.insert(1, "L'Allemagne Moderne");
    book_hashmap.insert(1, "Le Petit Prince");
    book_hashmap.insert(1, "섀도우 오브 유어 스마일");
    book_hashmap.insert(1, "Eye of the World");

    println!("{:?}", book_hashmap.get(&1));
}

這將打印出 Some("Eye of the World"),因為它是你最後使用 .insert() 的條目。

檢查一個條目是否存在是很容易的,因為你可以用 .get() 檢查,它給出了 Option

use std::collections::HashMap;

fn main() {
    let mut book_hashmap = HashMap::new();

    book_hashmap.insert(1, "L'Allemagne Moderne");

    if book_hashmap.get(&1).is_none() { // is_none() returns a bool: true if it's None, false if it's Some
        book_hashmap.insert(1, "Le Petit Prince");
    }

    println!("{:?}", book_hashmap.get(&1));
}

這個打印Some("L\'Allemagne Moderne")是因為已經有了key為1的,所以我們沒有插入Le Petit Prince

HashMap有一個非常有趣的方法,叫做.entry(),你一定要試試。有了它,你可以在沒有鍵的情況下,用如.or_insert()這類方法來插入值。有趣的是,它還給出了一個可變引用,所以如果你想的話,你可以改變它。首先是一個例子,我們只是在每次插入書名到HashMap時插入一個true

讓我們假設我們有一個圖書館,並希望跟蹤我們的書籍。

use std::collections::HashMap;

fn main() {
    let book_collection = vec!["L'Allemagne Moderne", "Le Petit Prince", "Eye of the World", "Eye of the World"]; // Eye of the World appears twice

    let mut book_hashmap = HashMap::new();

    for book in book_collection {
        book_hashmap.entry(book).or_insert(true);
    }
    for (book, true_or_false) in book_hashmap {
        println!("Do we have {}? {}", book, true_or_false);
    }
}

這個將打印:

Do we have Eye of the World? true
Do we have Le Petit Prince? true
Do we have L'Allemagne Moderne? true

但這並不是我們想要的。也許最好是數一下書的數量,這樣我們就知道世界之眼 有兩本。首先讓我們看看.entry()做了什麼,以及.or_insert()做了什麼。.entry()其實是返回了一個名為Entryenum

#![allow(unused)]
fn main() {
pub fn entry(&mut self, key: K) -> Entry<K, V> // 🚧
}

Entry文檔頁。下面是其代碼的簡單版本。K表示key,V表示value。

#![allow(unused)]
fn main() {
// 🚧
use std::collections::hash_map::*;

enum Entry<K, V> {
    Occupied(OccupiedEntry<K, V>),
    Vacant(VacantEntry<K, V>),
}
}

然後當我們調用.or_insert()時,它就會查看枚舉,並決定該怎麼做。

#![allow(unused)]
fn main() {
fn or_insert(self, default: V) -> &mut V { // 🚧
    match self {
        Occupied(entry) => entry.into_mut(),
        Vacant(entry) => entry.insert(default),
    }
}
}

有趣的是,它返回一個mut的引用。&mut V. 這意味著你可以使用let將其附加到一個變量上,並改變變量來改變HashMap中的值。所以對於每本書,如果沒有條目,我們就會插入一個0。而如果有的話,我們將在引用上使用+= 1來增加數字。現在它看起來像這樣:

use std::collections::HashMap;

fn main() {
    let book_collection = vec!["L'Allemagne Moderne", "Le Petit Prince", "Eye of the World", "Eye of the World"];

    let mut book_hashmap = HashMap::new();

    for book in book_collection {
        let return_value = book_hashmap.entry(book).or_insert(0); // return_value is a mutable reference. If nothing is there, it will be 0
        *return_value +=1; // Now return_value is at least 1. And if there was another book, it will go up by 1
    }

    for (book, number) in book_hashmap {
        println!("{}, {}", book, number);
    }
}

重要的部分是let return_value = book_hashmap.entry(book).or_insert(0);。如果去掉 let,你會得到 book_hashmap.entry(book).or_insert(0)。如果沒有let,它什麼也不做:它插入了0,沒有獲取指向0的可變引用。所以我們把它綁定到return_value上,這樣我們就可以保留0。然後我們把值增加1,這樣HashMap中的每本書都至少有1。然後當.entry()再看世界之眼時,它不會插入任何東西,但它給我們一個可變的1。然後我們把它增加到2,所以它才會打印出這樣的結果。

L'Allemagne Moderne, 1
Le Petit Prince, 1
Eye of the World, 2

你也可以用.or_insert()做一些事情,比如插入一個vec,然後推入數據。讓我們假設我們問街上的男人和女人他們對一個政治家的看法。他們給出的評分從0到10。然後我們要把這些數字放在一起,看看這個政治家是更受男人歡迎還是女人歡迎。它可以是這樣的。

use std::collections::HashMap;

fn main() {
    let data = vec![ // This is the raw data
        ("male", 9),
        ("female", 5),
        ("male", 0),
        ("female", 6),
        ("female", 5),
        ("male", 10),
    ];

    let mut survey_hash = HashMap::new();

    for item in data { // This gives a tuple of (&str, i32)
        survey_hash.entry(item.0).or_insert(Vec::new()).push(item.1); // This pushes the number into the Vec inside
    }

    for (male_or_female, numbers) in survey_hash {
        println!("{:?}: {:?}", male_or_female, numbers);
    }
}

這個打印:

"female", [5, 6, 5]
"male", [9, 0, 10]

重要的一行是:survey_hash.entry(item.0).or_insert(Vec::new()).push(item.1);,所以如果它看到 "女",就會檢查HashMap中是否已經有 "女"。如果沒有,它就會插入一個Vec::new(),然後把數字推入。如果它看到 "女性"已經在HashMap中,它將不會插入一個新的Vec,而只是將數字推入其中。

HashSet和BTreeSet

HashSet實際上是一個只有key的HashMap。在HashSet的頁面上面有解釋。

A hash set implemented as a HashMap where the value is (). 所以這是一個HashMap,有鍵,沒有值。

如果你只是想知道一個鍵是否存在,或者不存在,你經常會使用HashSet

想象一下,你有100個隨機數,每個數字在1和100之間。如果你這樣做,有些數字會出現不止一次,而有些數字根本不會出現。如果你把它們放到HashSet中,那麼你就會有一個所有出現的數字的列表。

use std::collections::HashSet;

fn main() {
    let many_numbers = vec![
        94, 42, 59, 64, 32, 22, 38, 5, 59, 49, 15, 89, 74, 29, 14, 68, 82, 80, 56, 41, 36, 81, 66,
        51, 58, 34, 59, 44, 19, 93, 28, 33, 18, 46, 61, 76, 14, 87, 84, 73, 71, 29, 94, 10, 35, 20,
        35, 80, 8, 43, 79, 25, 60, 26, 11, 37, 94, 32, 90, 51, 11, 28, 76, 16, 63, 95, 13, 60, 59,
        96, 95, 55, 92, 28, 3, 17, 91, 36, 20, 24, 0, 86, 82, 58, 93, 68, 54, 80, 56, 22, 67, 82,
        58, 64, 80, 16, 61, 57, 14, 11];

    let mut number_hashset = HashSet::new();

    for number in many_numbers {
        number_hashset.insert(number);
    }

    let hashset_length = number_hashset.len(); // The length tells us how many numbers are in it
    println!("There are {} unique numbers, so we are missing {}.", hashset_length, 100 - hashset_length);

    // Let's see what numbers we are missing
    let mut missing_vec = vec![];
    for number in 0..100 {
        if number_hashset.get(&number).is_none() { // If .get() returns None,
            missing_vec.push(number);
        }
    }

    print!("It does not contain: ");
    for number in missing_vec {
        print!("{} ", number);
    }
}

這個打印:

There are 66 unique numbers, so we are missing 34.
It does not contain: 1 2 4 6 7 9 12 21 23 27 30 31 39 40 45 47 48 50 52 53 62 65 69 70 72 75 77 78 83 85 88 97 98 99

BTreeSetHashSet相似,就像BTreeMapHashMap相似一樣。如果我們把HashSet中的每一項都打印出來,就不知道順序是什麼了。

#![allow(unused)]
fn main() {
for entry in number_hashset { // 🚧
    print!("{} ", entry);
}
}

也許它能打印出這個。67 28 42 25 95 59 87 11 5 81 64 34 8 15 13 86 10 89 63 93 49 41 46 57 60 29 17 22 74 43 32 38 36 76 71 18 14 84 61 16 35 90 56 54 91 19 94 44 3 0 68 80 51 92 24 20 82 26 58 33 55 96 37 66 79 73. 但它幾乎不會再以同樣的方式打印。

在這裡也一樣,如果你決定需要訂購的話,很容易把你的HashSet改成BTreeSet。在我們的代碼中,我們只需要做兩處改動,就可以從HashSet切換到BTreeSet

use std::collections::BTreeSet; // Change HashSet to BTreeSet

fn main() {
    let many_numbers = vec![
        94, 42, 59, 64, 32, 22, 38, 5, 59, 49, 15, 89, 74, 29, 14, 68, 82, 80, 56, 41, 36, 81, 66,
        51, 58, 34, 59, 44, 19, 93, 28, 33, 18, 46, 61, 76, 14, 87, 84, 73, 71, 29, 94, 10, 35, 20,
        35, 80, 8, 43, 79, 25, 60, 26, 11, 37, 94, 32, 90, 51, 11, 28, 76, 16, 63, 95, 13, 60, 59,
        96, 95, 55, 92, 28, 3, 17, 91, 36, 20, 24, 0, 86, 82, 58, 93, 68, 54, 80, 56, 22, 67, 82,
        58, 64, 80, 16, 61, 57, 14, 11];

    let mut number_btreeset = BTreeSet::new(); // Change HashSet to BTreeSet

    for number in many_numbers {
        number_btreeset.insert(number);
    }
    for entry in number_btreeset {
        print!("{} ", entry);
    }
}

現在會按順序打印。0 3 5 8 10 11 13 14 15 16 17 18 19 20 22 24 25 26 28 29 32 33 34 35 36 37 38 41 42 43 44 46 49 51 54 55 56 57 58 59 60 61 63 64 66 67 68 71 73 74 76 79 80 81 82 84 86 87 89 90 91 92 93 94 95 96.

二叉堆

BinaryHeap是一種有趣的集合類型,因為它大部分是無序的,但也有一點秩序。它把最大的元素放在前面,但其他元素是按任何順序排列的。

我們將用另一個元素列表來舉例,但這次數據少些。

use std::collections::BinaryHeap;

fn show_remainder(input: &BinaryHeap<i32>) -> Vec<i32> { // This function shows the remainder in the BinaryHeap. Actually an iterator would be
                                                         // faster than a function - we will learn them later.
    let mut remainder_vec = vec![];
    for number in input {
        remainder_vec.push(*number)
    }
    remainder_vec
}

fn main() {
    let many_numbers = vec![0, 5, 10, 15, 20, 25, 30]; // These numbers are in order

    let mut my_heap = BinaryHeap::new();

    for number in many_numbers {
        my_heap.push(number);
    }

    while let Some(number) = my_heap.pop() { // .pop() returns Some(number) if a number is there, None if not. It pops from the front
        println!("Popped off {}. Remaining numbers are: {:?}", number, show_remainder(&my_heap));
    }
}

這個打印:

Popped off 30. Remaining numbers are: [25, 15, 20, 0, 10, 5]
Popped off 25. Remaining numbers are: [20, 15, 5, 0, 10]
Popped off 20. Remaining numbers are: [15, 10, 5, 0]
Popped off 15. Remaining numbers are: [10, 0, 5]
Popped off 10. Remaining numbers are: [5, 0]
Popped off 5. Remaining numbers are: [0]
Popped off 0. Remaining numbers are: []

你可以看到,0指數的數字總是最大的。25, 20, 15, 10, 5, 然後是0.

使用BinaryHeap<(u8, &str)>的一個好方法是用於一個事情的集合。這裡我們創建一個BinaryHeap<(u8, &str)>,其中u8是任務重要性的數字。&str是對要做的事情的描述。

use std::collections::BinaryHeap;

fn main() {
    let mut jobs = BinaryHeap::new();

    // Add jobs to do throughout the day
    jobs.push((100, "Write back to email from the CEO"));
    jobs.push((80, "Finish the report today"));
    jobs.push((5, "Watch some YouTube"));
    jobs.push((70, "Tell your team members thanks for always working hard"));
    jobs.push((30, "Plan who to hire next for the team"));

    while let Some(job) = jobs.pop() {
        println!("You need to: {}", job.1);
    }
}

這將一直打印:

You need to: Write back to email from the CEO
You need to: Finish the report today
You need to: Tell your team members thanks for always working hard
You need to: Plan who to hire next for the team
You need to: Watch some YouTube

VecDeque

VecDeque就是一個Vec,既能從前面彈出item,又能從後面彈出item。Rust有VecDeque是因為Vec很適合從後面(最後一個元素)彈出,但從前面彈出就不那麼好了。當你在Vec上使用.pop()的時候,它只是把右邊最後一個item取下來,其他的都不會動。但是如果你把它從其他部分取下來,右邊的所有元素都會向左移動一個位置。你可以在.remove()的描述中看到這一點。

Removes and returns the element at position index within the vector, shifting all elements after it to the left.

所以如果你這樣做:

fn main() {
    let mut my_vec = vec![9, 8, 7, 6, 5];
    my_vec.remove(0);
}

它將刪除 9。索引1中的8將移到索引0,索引2中的7將移到索引1,以此類推。想象一下,一個大停車場,每當有一輛車離開時,右邊所有的車都要移過來。

比如說,這對計算機來說是一個很大的工作量。事實上,如果你在playground上運行它,它可能會因為工作太多而直接放棄。

fn main() {
    let mut my_vec = vec![0; 600_000];
    for i in 0..600000 {
        my_vec.remove(0);
    }
}

這是60萬個零的Vec。每次你用remove(0),它就會把每個零向左移動一個空格。然後它就會做60萬次。

VecDeque就不用擔心這個問題了。它通常比Vec慢一點,但如果你要在兩端都做事情,那麼它就快多了。你可以直接用VecDeque::fromVec來創建一個。那麼我們上面的代碼就是這樣的。

use std::collections::VecDeque;

fn main() {
    let mut my_vec = VecDeque::from(vec![0; 600000]);
    for i in 0..600000 {
        my_vec.pop_front(); // pop_front is like .pop but for the front
    }
}

現在速度快了很多,在playground上,它在一秒內完成,而不是放棄。

在接下來的這個例子中,我們在一個Vec上做一些事。我們創建一個VecDeque,用.push_front()把它們放在前面,所以我們添加的第一個元素會在右邊。但是我們推送的每一個元素都是一個(&str, bool):&str是描述, false表示還沒有完成。我們用done()函數從後面彈出一個元素,但是我們不想刪除它。相反,我們把false改成true,然後把它推到前面,這樣我們就可以保留它。

它看起來是這樣的:

use std::collections::VecDeque;

fn check_remaining(input: &VecDeque<(&str, bool)>) { // Each item is a (&str, bool)
    for item in input {
        if item.1 == false {
            println!("You must: {}", item.0);
        }
    }
}

fn done(input: &mut VecDeque<(&str, bool)>) {
    let mut task_done = input.pop_back().unwrap(); // pop off the back
    task_done.1 = true;                            // now it's done - mark as true
    input.push_front(task_done);                   // put it at the front now
}

fn main() {
    let mut my_vecdeque = VecDeque::new();
    let things_to_do = vec!["send email to customer", "add new product to list", "phone Loki back"];

    for thing in things_to_do {
        my_vecdeque.push_front((thing, false));
    }

    done(&mut my_vecdeque);
    done(&mut my_vecdeque);

    check_remaining(&my_vecdeque);

    for task in my_vecdeque {
        print!("{:?} ", task);
    }
}

這個打印:

You must: phone Loki back
("add new product to list", true) ("send email to customer", true) ("phone Loki back", false)

?操作符

有一種更短的方法來處理Result(和Option),它比matchif let更短。它叫做 "問號運算符",就是?。在返回結果的函數後,可以加上?。這樣就會:

  • 如果是Ok,返回Result裡面的內容。
  • 如果是Err,則將錯誤傳回。

換句話說,它幾乎為你做了所有的事情。

我們可以用 .parse() 再試一次。我們將編寫一個名為 parse_str 的函數,試圖將 &str 變成 i32。它看起來像這樣:

use std::num::ParseIntError;

fn parse_str(input: &str) -> Result<i32, ParseIntError> {
    let parsed_number = input.parse::<i32>()?; // Here is the question mark
    Ok(parsed_number)
}

fn main() {}

這個函數接收一個 &str。如果是 Ok,則給出一個 i32,包裹在 Ok 中。如果是 Err,則返回 ParseIntError。然後我們嘗試解析這個數字,並加上?。也就是 "檢查是否錯誤,如果沒問題就給出Result裡面的內容"。如果有問題,就會返回錯誤並結束。但如果沒問題,就會進入下一行。下一行是Ok()裡面的數字。我們需要用Ok來包裝,因為返回的是Result<i32, ParseIntError>,而不是i32

現在,我們可以試試我們的函數。讓我們看看它對&str的vec有什麼作用。

fn parse_str(input: &str) -> Result<i32, std::num::ParseIntError> {
    let parsed_number = input.parse::<i32>()?;
    Ok(parsed_number)
}

fn main() {
    let str_vec = vec!["Seven", "8", "9.0", "nice", "6060"];
    for item in str_vec {
        let parsed = parse_str(item);
        println!("{:?}", parsed);
    }
}

這個打印:

Err(ParseIntError { kind: InvalidDigit })
Ok(8)
Err(ParseIntError { kind: InvalidDigit })
Err(ParseIntError { kind: InvalidDigit })
Ok(6060)

我們是怎麼找到std::num::ParseIntError的呢?一個簡單的方法就是再 "問"一下編譯器。

fn main() {
    let failure = "Not a number".parse::<i32>();
    failure.rbrbrb(); // ⚠️ Compiler: "What is rbrbrb()???"
}

編譯器不懂,說。

error[E0599]: no method named `rbrbrb` found for enum `std::result::Result<i32, std::num::ParseIntError>` in the current scope
 --> src\main.rs:3:13
  |
3 |     failure.rbrbrb();
  |             ^^^^^^ method not found in `std::result::Result<i32, std::num::ParseIntError>`

所以std::result::Result<i32, std::num::ParseIntError>就是我們需要的簽名。

我們不需要寫 std::result::Result,因為 Result 總是 "在範圍內"(在範圍內 = 準備好使用)。Rust對我們經常使用的所有類型都是這樣做的,所以我們不必寫std::result::Resultstd::collections::Vec等。

我們現在還沒有處理文件這樣的東西,所以?操作符看起來還不是太有用。但這裡有一個無用但快速的例子,說明你如何在單行上使用它。與其用 .parse() 創建一個 i32,不如做更多。我們將創建一個 u16,然後把它變成 String,再變成 u32,然後再變成 String,最後變成 i32

use std::num::ParseIntError;

fn parse_str(input: &str) -> Result<i32, ParseIntError> {
    let parsed_number = input.parse::<u16>()?.to_string().parse::<u32>()?.to_string().parse::<i32>()?; // Add a ? each time to check and pass it on
    Ok(parsed_number)
}

fn main() {
    let str_vec = vec!["Seven", "8", "9.0", "nice", "6060"];
    for item in str_vec {
        let parsed = parse_str(item);
        println!("{:?}", parsed);
    }
}

這打印出同樣的東西,但這次我們在一行中處理了三個Result。稍後我們將對文件進行處理,因為它們總是返回Result,因為很多事情都可能出錯。

想象一下:你想打開一個文件,向它寫入,然後關閉它。首先你需要成功找到這個文件(這就是一個Result)。然後你需要成功地寫入它(那是一個Result)。對於?,你可以在一行上完成。

When panic and unwrap are good

Rust有一個panic!的宏,你可以用它來讓程序崩潰。它使用起來很方便。

fn main() {
    panic!("Time to panic!");
}

運行程序時,會顯示信息"Time to panic!"thread 'main' panicked at 'Time to panic!', src\main.rs:2:3

你會記得src\main.rs是目錄和文件名,2:3是行名和列名。有了這些信息,你就可以找到代碼並修復它。

panic!是一個很好用的宏,以確保你知道什麼時候有變化。例如,這個叫做prints_three_things的函數總是從一個向量中打印出索引[0]、[1]和[2]。這沒關係,因為我們總是給它一個有三個元素的向量。

fn prints_three_things(vector: Vec<i32>) {
    println!("{}, {}, {}", vector[0], vector[1], vector[2]);
}

fn main() {
    let my_vec = vec![8, 9, 10];
    prints_three_things(my_vec);
}

它打印出8, 9, 10,一切正常。

但試想一下,後來我們寫的代碼越來越多,忘記了my_vec只能有三個元素。現在my_vec在這部分有六個元素。

fn prints_three_things(vector: Vec<i32>) {
  println!("{}, {}, {}", vector[0], vector[1], vector[2]);
}

fn main() {
  let my_vec = vec![8, 9, 10, 10, 55, 99]; // Now my_vec has six things
  prints_three_things(my_vec);
}

不會發生錯誤,因為[0]和[1]和[2]都在這個較長的Vec裡面。但如果只能有三個元素呢?我們就不會知道有問題了,因為程序不會崩潰。我們應該這樣做:

fn prints_three_things(vector: Vec<i32>) {
    if vector.len() != 3 {
        panic!("my_vec must always have three items") // will panic if the length is not 3
    }
    println!("{}, {}, {}", vector[0], vector[1], vector[2]);
}

fn main() {
    let my_vec = vec![8, 9, 10];
    prints_three_things(my_vec);
}

現在我們知道,如果向量有6個元素,它應該要崩潰:

    // ⚠️
fn prints_three_things(vector: Vec<i32>) {
    if vector.len() != 3 {
        panic!("my_vec must always have three items")
    }
    println!("{}, {}, {}", vector[0], vector[1], vector[2]);
}

fn main() {
    let my_vec = vec![8, 9, 10, 10, 55, 99];
    prints_three_things(my_vec);
}

這樣我們就得到了thread 'main' panicked at 'my_vec must always have three items', src\main.rs:8:9。多虧了panic!,我們現在記得my_vec應該只有三個元素。所以panic!是一個很好的宏,可以在你的代碼中創建提醒。

還有三個與panic!類似的宏,你在測試中經常使用。它們分別是 assert!, assert_eq!, 和 assert_ne!.

下面是它們的意思。

  • assert!(): 如果()裡面的部分不是真的, 程序就會崩潰.
  • assert_eq!():()裡面的兩個元素必須相等。
  • assert_ne!():()裡面的兩個元素必須不相等。(ne表示不相等)

一些例子。

fn main() {
    let my_name = "Loki Laufeyson";

    assert!(my_name == "Loki Laufeyson");
    assert_eq!(my_name, "Loki Laufeyson");
    assert_ne!(my_name, "Mithridates");
}

這不會有任何作用,因為三個斷言宏都沒有問題。(這就是我們想要的)

如果你願意,還可以加個提示信息。

fn main() {
    let my_name = "Loki Laufeyson";

    assert!(
        my_name == "Loki Laufeyson",
        "{} should be Loki Laufeyson",
        my_name
    );
    assert_eq!(
        my_name, "Loki Laufeyson",
        "{} and Loki Laufeyson should be equal",
        my_name
    );
    assert_ne!(
        my_name, "Mithridates",
        "You entered {}. Input must not equal Mithridates",
        my_name
    );
}

這些信息只有在程序崩潰時才會顯示。所以如果你運行這個。

fn main() {
    let my_name = "Mithridates";

    assert_ne!(
        my_name, "Mithridates",
        "You enter {}. Input must not equal Mithridates",
        my_name
    );
}

它將顯示:

thread 'main' panicked at 'assertion failed: `(left != right)`
  left: `"Mithridates"`,
 right: `"Mithridates"`: You entered Mithridates. Input must not equal Mithridates', src\main.rs:4:5

所以它說 "你說左!=右,但左==右"。而且它顯示我們的信息說You entered Mithridates. Input must not equal Mithridates

當你在寫程序的時候,想讓它在出現問題的時候崩潰,unwrap是個好注意。當你的代碼寫完後,把unwrap改成其他不會崩潰的東西就好了。

你也可以用expect,它和unwrap一樣,但是更好一些,因為它支持用戶自定義信息。教科書通常會給出這樣的建議:"如果你經常使用.unwrap(), 至少也要用.expect()來獲得更好的錯誤信息."

這樣會崩潰的:

   // ⚠️
fn get_fourth(input: &Vec<i32>) -> i32 {
    let fourth = input.get(3).unwrap();
    *fourth
}

fn main() {
    let my_vec = vec![9, 0, 10];
    let fourth = get_fourth(&my_vec);
}

錯誤信息是thread 'main' panicked at 'called Option::unwrap() on a None value', src\main.rs:7:18

現在我們用expect來寫自己的信息。

   // ⚠️
fn get_fourth(input: &Vec<i32>) -> i32 {
    let fourth = input.get(3).expect("Input vector needs at least 4 items");
    *fourth
}

fn main() {
    let my_vec = vec![9, 0, 10];
    let fourth = get_fourth(&my_vec);
}

又崩潰了,但錯誤比較多。thread 'main' panicked at 'Input vector needs at least 4 items', src\main.rs:7:18. .expect()因為這個原因比.unwrap()要好一點,但是在None上還是會崩潰。現在這裡有一個錯誤的案例,一個函數試圖unwrap兩次。它需要一個Vec<Option<i32>>,所以可能每個部分都會有一個Some<i32>,也可能是一個None

fn try_two_unwraps(input: Vec<Option<i32>>) {
    println!("Index 0 is: {}", input[0].unwrap());
    println!("Index 1 is: {}", input[1].unwrap());
}

fn main() {
    let vector = vec![None, Some(1000)]; // This vector has a None, so it will panic
    try_two_unwraps(vector);
}

消息是:thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src\main.rs:2:32。我們不檢查行號,就不知道是第一個.unwrap()還是第二個.unwrap()。最好是檢查一下長度,也不要unwrap。不過有了.expect()至少會好一點。下面是.expect()的情況:

fn try_two_unwraps(input: Vec<Option<i32>>) {
    println!("Index 0 is: {}", input[0].expect("The first unwrap had a None!"));
    println!("Index 1 is: {}", input[1].expect("The second unwrap had a None!"));
}

fn main() {
    let vector = vec![None, Some(1000)];
    try_two_unwraps(vector);
}

所以,這是好一點的。thread 'main' panicked at 'The first unwrap had a None!', src\main.rs:2:32. 我們也有行號,所以我們可以找到它。

如果你想一直有一個你想選擇的值,也可以用unwrap_or。如果你這樣做,它永遠不會崩潰。就是這樣的。

  • 1)好,因為你的程序不會崩潰,但
  • 2)如果你想讓程序在出現問題時崩潰,也許不好。

但通常我們都不希望自己的程序崩潰,所以unwrap_or是個不錯的方法。

fn main() {
    let my_vec = vec![8, 9, 10];

    let fourth = my_vec.get(3).unwrap_or(&0); // If .get doesn't work, we will make the value &0.
                                              // .get returns a reference, so we need &0 and not 0
                                              // You can write "let *fourth" with a * if you want fourth to be
                                              // a 0 and not a &0, but here we just print so it doesn't matter

    println!("{}", fourth);
}

這將打印出 0,因為 .unwrap_or(&0) 給出了一個 0,即使它是 None

特性

我們以前見過trait:DebugCopyClone都是trait。要給一個類型一個trait,就必須實現它。因為Debug和其他的trait都很常見,所以我們有自動實現的屬性。這就是當你寫下#[derive(Debug)]所發生的事情:你自動實現了Debug

#[derive(Debug)]
struct MyStruct {
    number: usize,
}

fn main() {}

但是其他的特性就比較困難了,所以需要用impl手動實現。例如,Add(在std::ops::Add處找到)是用來累加兩個東西的。但是Rust並不知道你到底要怎麼累加,所以你必須告訴它。

struct ThingsToAdd {
    first_thing: u32,
    second_thing: f32,
}

fn main() {}

我們可以累加first_thingsecond_thing,但我們需要提供更多信息。也許我們想要一個f32,所以像這樣:

#![allow(unused)]
fn main() {
// 🚧
let result = self.second_thing + self.first_thing as f32
}

但也許我們想要一個整數,所以像這樣:

#![allow(unused)]
fn main() {
// 🚧
let result = self.second_thing as u32 + self.first_thing
}

或者我們想把self.first_thing放在self.second_thing旁邊,這樣加。所以如果我們把55加到33.4,我們要看到的是5533.4,而不是88.4。

所以首先我們看一下如何創建一個trait。關於trait,要記住的重要一點是,它們是關於行為的。要創建一個trait,寫下單詞trait,然後創建一些函數。

struct Animal { // A simple struct - an Animal only has a name
    name: String,
}

trait Dog { // The dog trait gives some functionality
    fn bark(&self) { // It can bark
        println!("Woof woof!");
    }
    fn run(&self) { // and it can run
        println!("The dog is running!");
    }
}

impl Dog for Animal {} // Now Animal has the trait Dog

fn main() {
    let rover = Animal {
        name: "Rover".to_string(),
    };

    rover.bark(); // Now Animal can use bark()
    rover.run();  // and it can use run()
}

這個是可以的,但是我們不想打印 "狗在跑"。如果你想的話,你可以改變trait給你的方法,但你必須有相同的簽名。這意味著它需要接受同樣的東西,並返回同樣的東西。例如,我們可以改變 .run() 的方法,但我們必須遵循簽名。簽名說

#![allow(unused)]
fn main() {
// 🚧
fn run(&self) {
    println!("The dog is running!");
}
}

fn run(&self)的意思是 "fn run()&self為參數,不返回任何內容"。所以你不能這樣做:

#![allow(unused)]
fn main() {
fn run(&self) -> i32 { // ⚠️
    5
}
}

Rust會說。

   = note: expected fn pointer `fn(&Animal)`
              found fn pointer `fn(&Animal) -> i32`

但我們可以做到這一點。

struct Animal { // A simple struct - an Animal only has a name
    name: String,
}

trait Dog { // The dog trait gives some functionality
    fn bark(&self) { // It can bark
        println!("Woof woof!");
    }
    fn run(&self) { // and it can run
        println!("The dog is running!");
    }
}

impl Dog for Animal {
    fn run(&self) {
        println!("{} is running!", self.name);
    }
}

fn main() {
    let rover = Animal {
        name: "Rover".to_string(),
    };

    rover.bark(); // Now Animal can use bark()
    rover.run();  // and it can use run()
}

現在它打印的是 Rover is running!。這是好的,因為我們返回的是 (),或者說什麼都沒有,這就是trait所說的。

當你寫一個trait的時候,你可以直接寫函數簽名,但如果你這樣做,用戶將不得不寫函數實現。我們來試試。現在我們把bark()run()改成只說fn bark(&self);fn run(&self);。這不是一個完整的函數實現,所以必須由用戶來寫。

struct Animal {
    name: String,
}

trait Dog {
    fn bark(&self); // bark() says it needs a &self and returns nothing
    fn run(&self); // run() says it needs a &self and returns nothing.
                   // So now we have to write them ourselves.
}

impl Dog for Animal {
    fn bark(&self) {
        println!("{}, stop barking!!", self.name);
    }
    fn run(&self) {
        println!("{} is running!", self.name);
    }
}

fn main() {
    let rover = Animal {
        name: "Rover".to_string(),
    };

    rover.bark();
    rover.run();
}

所以,當你創建一個trait時,你必須思考:"我應該寫哪些功能?而用戶應該寫哪些函數?" 如果你認為用戶每次使用函數的方式應該是一樣的,那麼就把函數寫出來。如果你認為用戶會以不同的方式使用,那就寫出函數簽名即可。

所以,讓我們嘗試為我們的struct實現Display特性。首先我們將創建一個簡單的結構體:

struct Cat {
    name: String,
    age: u8,
}

fn main() {
    let mr_mantle = Cat {
        name: "Reggie Mantle".to_string(),
        age: 4,
    };
}

現在我們要打印mr_mantle。調試很容易得出。

#[derive(Debug)]
struct Cat {
    name: String,
    age: u8,
}

fn main() {
    let mr_mantle = Cat {
        name: "Reggie Mantle".to_string(),
        age: 4,
    };

    println!("Mr. Mantle is a {:?}", mr_mantle);
}

但Debug打印不是最漂亮的方式,因為它看起來是這樣的:

Mr. Mantle is a Cat { name: "Reggie Mantle", age: 4 }

因此,如果我們想要更好的打印,就需要實現DisplayCat。在https://doc.rust-lang.org/std/fmt/trait.Display.html上我們可以看到Display的信息,還有一個例子。它說

use std::fmt;

struct Position {
    longitude: f32,
    latitude: f32,
}

impl fmt::Display for Position {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.longitude, self.latitude)
    }
}

fn main() {}

有些部分我們還不明白,比如<'_>f在做什麼。但我們理解Position結構體:它只是兩個f32。我們也明白,self.longitudeself.latitude是結構體中的字段。所以,也許我們的結構體就可以用這個代碼,用self.nameself.age。另外,write!看起來很像println!,所以很熟悉。所以我們這樣寫。

use std::fmt;

struct Cat {
    name: String,
    age: u8,
}

impl fmt::Display for Cat {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} is a cat who is {} years old.", self.name, self.age)
    }
}

fn main() {}

讓我們添加一個fn main()。現在我們的代碼是這樣的。

use std::fmt;

struct Cat {
    name: String,
    age: u8,
}

impl fmt::Display for Cat {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
      write!(f, "{} is a cat who is {} years old.", self.name, self.age)
  }
}

fn main() {
    let mr_mantle = Cat {
        name: "Reggie Mantle".to_string(),
        age: 4,
    };

    println!("{}", mr_mantle);
}

成功了! 現在,當我們使用{}打印時,我們得到Reggie Mantle is a cat who is 4 years old.。這看起來好多了。

順便說一下,如果你實現了Display,那麼你就可以免費得到ToString的特性。這是因為你使用format!宏來實現.fmt()函數,這讓你可以用.to_string()來創建一個String。所以我們可以做這樣的事情,我們把reggie_mantle傳給一個想要String的函數,或者其他任何東西。

use std::fmt;
struct Cat {
    name: String,
    age: u8,
}

impl fmt::Display for Cat {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} is a cat who is {} years old.", self.name, self.age)
    }
}

fn print_cats(pet: String) {
    println!("{}", pet);
}

fn main() {
    let mr_mantle = Cat {
        name: "Reggie Mantle".to_string(),
        age: 4,
    };

    print_cats(mr_mantle.to_string()); // Turn him into a String here
    println!("Mr. Mantle's String is {} letters long.", mr_mantle.to_string().chars().count()); // Turn him into chars and count them
}

這個打印:

Reggie Mantle is a cat who is 4 years old.
Mr. Mantle's String is 42 letters long.

關於trait,要記住的是,它們是關於某些東西的行為。你的struct是如何行動的?它能做什麼?這就是trait的作用。如果你想想我們到目前為止所看到的一些trait,它們都是關於行為的:Copy是一個類型可以做的事情。Display也是一個類型能做的事情。ToString是另一個trait,它也是一個類型可以做的事情:它可以變化成一個String。在我們的 Dog trait中,Dog這個詞並不意味著你能做的事情,但它給出了一些讓它做事情的方法。 你也可以為 struct Poodlestruct Beagle 實現它,它們都會得到 Dog 方法。

讓我們再看一個與單純行為聯繫更緊密的例子。我們將想象一個有一些簡單角色的幻想遊戲。一個是Monster,另外兩個是WizardRangerMonster只是有health,所以我們可以攻擊它,其他兩個還沒有什麼。但是我們做了兩個trait。一個叫FightClose,讓你近身作戰。另一個是FightFromDistance,讓你在遠處戰鬥。只有Ranger可以使用FightFromDistance。下面是它的樣子:

struct Monster {
    health: i32,
}

struct Wizard {}
struct Ranger {}

trait FightClose {
    fn attack_with_sword(&self, opponent: &mut Monster) {
        opponent.health -= 10;
        println!(
            "You attack with your sword. Your opponent now has {} health left.",
            opponent.health
        );
    }
    fn attack_with_hand(&self, opponent: &mut Monster) {
        opponent.health -= 2;
        println!(
            "You attack with your hand. Your opponent now has {} health left.",
            opponent.health
        );
    }
}
impl FightClose for Wizard {}
impl FightClose for Ranger {}

trait FightFromDistance {
    fn attack_with_bow(&self, opponent: &mut Monster, distance: u32) {
        if distance < 10 {
            opponent.health -= 10;
            println!(
                "You attack with your bow. Your opponent now has {} health left.",
                opponent.health
            );
        }
    }
    fn attack_with_rock(&self, opponent: &mut Monster, distance: u32) {
        if distance < 3 {
            opponent.health -= 4;
        }
        println!(
            "You attack with your rock. Your opponent now has {} health left.",
            opponent.health
        );
    }
}
impl FightFromDistance for Ranger {}

fn main() {
    let radagast = Wizard {};
    let aragorn = Ranger {};

    let mut uruk_hai = Monster { health: 40 };

    radagast.attack_with_sword(&mut uruk_hai);
    aragorn.attack_with_bow(&mut uruk_hai, 8);
}

這個打印:

You attack with your sword. Your opponent now has 30 health left.
You attack with your bow. Your opponent now has 20 health left.

我們在trait裡面一直傳遞self,但是我們現在不能用它做什麼。那是因為 Rust 不知道什麼類型會使用它。它可能是一個 Wizard,也可能是一個 Ranger,也可能是一個叫做 Toefocfgetobjtnode 的新結構,或者其他任何東西。為了讓self具有一定的功能,我們可以在trait中添加必要的trait。比如說,如果我們想用{:?}打印,那麼我們就需要Debug。你只要把它寫在:(冒號)後面,就可以把它添加到trait中。現在我們的代碼是這樣的。

struct Monster {
    health: i32,
}

#[derive(Debug)] // Now Wizard has Debug
struct Wizard {
    health: i32, // Now Wizard has health
}
#[derive(Debug)] // So does Ranger
struct Ranger {
    health: i32, // So does Ranger
}

trait FightClose: std::fmt::Debug { // Now a type needs Debug to use FightClose
    fn attack_with_sword(&self, opponent: &mut Monster) {
        opponent.health -= 10;
        println!(
            "You attack with your sword. Your opponent now has {} health left. You are now at: {:?}", // We can now print self with {:?} because we have Debug
            opponent.health, &self
        );
    }
    fn attack_with_hand(&self, opponent: &mut Monster) {
        opponent.health -= 2;
        println!(
            "You attack with your hand. Your opponent now has {} health left.  You are now at: {:?}",
            opponent.health, &self
        );
    }
}
impl FightClose for Wizard {}
impl FightClose for Ranger {}

trait FightFromDistance: std::fmt::Debug { // We could also do trait FightFromDistance: FightClose because FightClose needs Debug
    fn attack_with_bow(&self, opponent: &mut Monster, distance: u32) {
        if distance < 10 {
            opponent.health -= 10;
            println!(
                "You attack with your bow. Your opponent now has {} health left.  You are now at: {:?}",
                opponent.health, self
            );
        }
    }
    fn attack_with_rock(&self, opponent: &mut Monster, distance: u32) {
        if distance < 3 {
            opponent.health -= 4;
        }
        println!(
            "You attack with your rock. Your opponent now has {} health left.  You are now at: {:?}",
            opponent.health, self
        );
    }
}
impl FightFromDistance for Ranger {}

fn main() {
    let radagast = Wizard { health: 60 };
    let aragorn = Ranger { health: 80 };

    let mut uruk_hai = Monster { health: 40 };

    radagast.attack_with_sword(&mut uruk_hai);
    aragorn.attack_with_bow(&mut uruk_hai, 8);
}

現在這個打印:

You attack with your sword. Your opponent now has 30 health left. You are now at: Wizard { health: 60 }
You attack with your bow. Your opponent now has 20 health left.  You are now at: Ranger { health: 80 }

在真實的遊戲中,可能最好為每個類型重寫這個,因為You are now at: Wizard { health: 60 }看起來有點可笑。這也是為什麼trait裡面的方法通常很簡單,因為你不知道什麼類型會使用它。例如,你不能寫出 self.0 += 10 這樣的東西。但是這個例子表明,我們可以在我們正在寫的trait裡面使用其他的trait。當我們這樣做的時候,我們會得到一些我們可以使用的方法。

另外一種使用trait的方式是使用所謂的trait bounds。意思是 "通過一個trait進行限制"。trait限制很簡單,因為一個trait實際上不需要任何方法,或者說根本不需要任何東西。讓我們用類似但不同的東西重寫我們的代碼。這次我們的trait沒有任何方法,但我們有其他需要trait使用的函數。

use std::fmt::Debug;  // So we don't have to write std::fmt::Debug every time now

struct Monster {
    health: i32,
}

#[derive(Debug)]
struct Wizard {
    health: i32,
}
#[derive(Debug)]
struct Ranger {
    health: i32,
}

trait Magic{} // No methods for any of these traits. They are just trait bounds
trait FightClose {}
trait FightFromDistance {}

impl FightClose for Ranger{} // Each type gets FightClose,
impl FightClose for Wizard {}
impl FightFromDistance for Ranger{} // but only Ranger gets FightFromDistance
impl Magic for Wizard{}  // and only Wizard gets Magic

fn attack_with_bow<T: FightFromDistance + Debug>(character: &T, opponent: &mut Monster, distance: u32) {
    if distance < 10 {
        opponent.health -= 10;
        println!(
            "You attack with your bow. Your opponent now has {} health left.  You are now at: {:?}",
            opponent.health, character
        );
    }
}

fn attack_with_sword<T: FightClose + Debug>(character: &T, opponent: &mut Monster) {
    opponent.health -= 10;
    println!(
        "You attack with your sword. Your opponent now has {} health left. You are now at: {:?}",
        opponent.health, character
    );
}

fn fireball<T: Magic + Debug>(character: &T, opponent: &mut Monster, distance: u32) {
    if distance < 15 {
        opponent.health -= 20;
        println!("You raise your hands and cast a fireball! Your opponent now has {} health left. You are now at: {:?}",
    opponent.health, character);
    }
}

fn main() {
    let radagast = Wizard { health: 60 };
    let aragorn = Ranger { health: 80 };

    let mut uruk_hai = Monster { health: 40 };

    attack_with_sword(&radagast, &mut uruk_hai);
    attack_with_bow(&aragorn, &mut uruk_hai, 8);
    fireball(&radagast, &mut uruk_hai, 8);
}

這個打印出來的東西幾乎是一樣的。

You attack with your sword. Your opponent now has 30 health left. You are now at: Wizard { health: 60 }
You attack with your bow. Your opponent now has 20 health left.  You are now at: Ranger { health: 80 }
You raise your hands and cast a fireball! Your opponent now has 0 health left. You are now at: Wizard { health: 60 }

所以你可以看到,當你使用traits時,有很多方法可以做同樣的事情。這一切都取決於什麼對你正在編寫的程序最有意義。

現在讓我們來看看如何實現一些在Rust中使用的主要trait。

From trait

From是一個非常方便的trait,你知道這一點,因為你已經看到了很多。使用From,你可以從一個&str創建一個String,你也可以用許多其他類型創建多種類型。例如,Vec使用From來創建以下類型:

From<&'_ [T]>
From<&'_ mut [T]>
From<&'_ str>
From<&'a Vec<T>>
From<[T; N]>
From<BinaryHeap<T>>
From<Box<[T]>>
From<CString>
From<Cow<'a, [T]>>
From<String>
From<Vec<NonZeroU8>>
From<Vec<T>>
From<VecDeque<T>>

這裡有很多Vec::from()我們還沒有用過。我們來做幾個,看看會怎麼樣:

use std::fmt::Display; // We will make a generic function to print them so we want Display

fn print_vec<T: Display>(input: &Vec<T>) { // Take any Vec<T> if type T has Display
    for item in input {
        print!("{} ", item);
    }
    println!();
}

fn main() {

    let array_vec = Vec::from([8, 9, 10]); // Try from an array
    print_vec(&array_vec);

    let str_vec = Vec::from("What kind of vec will I be?"); // An array from a &str? This will be interesting
    print_vec(&str_vec);

    let string_vec = Vec::from("What kind of vec will a String be?".to_string()); // Also from a String
    print_vec(&string_vec);
}

它打印的內容如下。

8 9 10
87 104 97 116 32 107 105 110 100 32 111 102 32 118 101 99 32 119 105 108 108 32 73 32 98 101 63
87 104 97 116 32 107 105 110 100 32 111 102 32 118 101 99 32 119 105 108 108 32 97 32 83 116 114 105 110 103 32 98 101 63

如果從類型上看,第二個和第三個向量是Vec<u8>,也就是&strString的字節。所以你可以看到From是非常靈活的,用的也很多。我們用自己的類型來試試。

我們將創建兩個結構體,然後為其中一個結構體實現From。一個結構體將是City,另一個結構體將是Country。我們希望能夠做到這一點。let country_name = Country::from(vector_of_cities).

它看起來是這樣的:

#[derive(Debug)] // So we can print City
struct City {
    name: String,
    population: u32,
}

impl City {
    fn new(name: &str, population: u32) -> Self { // just a new function
        Self {
            name: name.to_string(),
            population,
        }
    }
}
#[derive(Debug)] // Country also needs to be printed
struct Country {
    cities: Vec<City>, // Our cities go in here
}

impl From<Vec<City>> for Country { // Note: we don't have to write From<City>, we can also do
                                   // From<Vec<City>>. So we can also implement on a type that
                                   // we didn't create
    fn from(cities: Vec<City>) -> Self {
        Self { cities }
    }
}

impl Country {
    fn print_cities(&self) { // function to print the cities in Country
        for city in &self.cities {
            // & because Vec<City> isn't Copy
            println!("{:?} has a population of {:?}.", city.name, city.population);
        }
    }
}

fn main() {
    let helsinki = City::new("Helsinki", 631_695);
    let turku = City::new("Turku", 186_756);

    let finland_cities = vec![helsinki, turku]; // This is the Vec<City>
    let finland = Country::from(finland_cities); // So now we can use From

    finland.print_cities();
}

這個將打印:

"Helsinki" has a population of 631695.
"Turku" has a population of 186756.

你可以看到,From很容易從你沒有創建的類型中實現,比如Veci32等等。這裡還有一個例子,我們創建一個有兩個向量的向量。第一個向量存放偶數,第二個向量存放奇數。對於From,你可以給它一個i32的向量,它會把它變成Vec<Vec<i32>>:一個容納i32的向量。

use std::convert::From;

struct EvenOddVec(Vec<Vec<i32>>);

impl From<Vec<i32>> for EvenOddVec {
    fn from(input: Vec<i32>) -> Self {
        let mut even_odd_vec: Vec<Vec<i32>> = vec![vec![], vec![]]; // A vec with two empty vecs inside
                                                                    // This is the return value but first we must fill it
        for item in input {
            if item % 2 == 0 {
                even_odd_vec[0].push(item);
            } else {
                even_odd_vec[1].push(item);
            }
        }
        Self(even_odd_vec) // Now it is done so we return it as Self (Self = EvenOddVec)
    }
}

fn main() {
    let bunch_of_numbers = vec![8, 7, -1, 3, 222, 9787, -47, 77, 0, 55, 7, 8];
    let new_vec = EvenOddVec::from(bunch_of_numbers);

    println!("Even numbers: {:?}\nOdd numbers: {:?}", new_vec.0[0], new_vec.0[1]);
}

這個打印:

Even numbers: [8, 222, 0, 8]
Odd numbers: [7, -1, 3, 9787, -47, 77, 55, 7]

EvenOddVec 這樣的類型可能最好是通用 T,這樣我們就可以使用許多數字類型。如果你想練習的話,你可以試著把這個例子做成通用的。

在函數中使用字符串和&str

有時你想讓一個函數可以同時接受 String&str。你可以通過泛型和 AsRef 特性來實現這一點。AsRef 用於從一個類型向另一個類型提供引用。如果你看看 String 的文檔,你可以看到它對許多類型都有 AsRef

https://doc.rust-lang.org/std/string/struct.String.html

下面是它們的一些函數簽名。

AsRef<str>:

#![allow(unused)]
fn main() {
// 🚧
impl AsRef<str> for String

fn as_ref(&self) -> &str
}

AsRef<[u8]>:

#![allow(unused)]
fn main() {
// 🚧
impl AsRef<[u8]> for String

fn as_ref(&self) -> &[u8]
}

AsRef<OsStr>:

#![allow(unused)]
fn main() {
// 🚧
impl AsRef<OsStr> for String

fn as_ref(&self) -> &OsStr
}

你可以看到,它需要&self,並給出另一個類型的引用。這意味著,如果你有一個通用類型T,你可以說它需要AsRef<str>。如果你這樣做,它將能夠使用一個&str和一個String

我們先說說泛型函數。這個還不能用。

fn print_it<T>(input: T) {
    println!("{}", input) // ⚠️
}

fn main() {
    print_it("Please print me");
}

Rust說error[E0277]: T doesn't implement std::fmt::Display。所以我們會要求T實現Display。

use std::fmt::Display;

fn print_it<T: Display>(input: T) {
    println!("{}", input)
}

fn main() {
    print_it("Please print me");
}

現在可以用了,打印出Please print me。這是好的,但T仍然可以是多種類型。 可以是i8,也可以是f32,或者其他任何實現了Display的類型。我們加上AsRef<str>,現在T需要AsRef<str>Display

use std::fmt::Display;

fn print_it<T: AsRef<str> + Display>(input: T) {
    println!("{}", input)
}

fn main() {
    print_it("Please print me");
    print_it("Also, please print me".to_string());
    // print_it(7); <- This will not print
}

現在,它不會接受i8這樣的類型。

不要忘了,當函數變長時,你可以用where來寫不同的函數。如果我們加上Debug,那麼就會變成fn print_it<T: AsRef<str> + Display + Debug>(input: T),這一行就很長了。所以我們可以這樣寫。

use std::fmt::{Debug, Display}; // add Debug

fn print_it<T>(input: T) // Now this line is easy to read
where
    T: AsRef<str> + Debug + Display, // and these traits are easy to read
{
    println!("{}", input)
}

fn main() {
    print_it("Please print me");
    print_it("Also, please print me".to_string());
}

鏈式方法

Rust是一種系統編程語言,就像C和C++一樣,它的代碼可以寫成獨立的命令,單獨成行,但它也有函數式風格。兩種風格都可以,但函數式通常比較短。下面以非函數式(稱為 "命令式")為例,讓Vec從1到10。

fn main() {
    let mut new_vec = Vec::new();
    let mut counter = 1;

    while counter < 11 {
        new_vec.push(counter);
        counter += 1;
    }

    println!("{:?}", new_vec);
}

這個打印[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

而這裡是函數式風格的例子:

fn main() {
    let new_vec = (1..=10).collect::<Vec<i32>>();
    // Or you can write it like this:
    // let new_vec: Vec<i32> = (1..=10).collect();
    println!("{:?}", new_vec);
}

.collect()可以為很多類型做集合,所以我們要告訴它類型。

用函數式可以鏈接方法。"鏈接方法"的意思是把很多方法放在一個語句中。下面是一個很多方法鏈在一起的例子。

fn main() {
    let my_vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let new_vec = my_vec.into_iter().skip(3).take(4).collect::<Vec<i32>>();

    println!("{:?}", new_vec);
}

這樣就創建了一個[3, 4, 5, 6]的Vec。這一行的信息量很大,所以把每個方法放在新的一行上會有幫助。讓我們這樣做,以使其更容易閱讀。

fn main() {
    let my_vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let new_vec = my_vec
        .into_iter() // "iterate" over the items (iterate = work with each item inside it). into_iter() gives us owned values, not references
        .skip(3) // skip over three items: 0, 1, and 2
        .take(4) // take the next four: 3, 4, 5, and 6
        .collect::<Vec<i32>>(); // put them in a new Vec<i32>

    println!("{:?}", new_vec);
}

當你瞭解閉包和迭代器時,你可以最好地使用這種函數式。所以我們接下來將學習它們。

迭代器

迭代器是一個構造,它可以給你集合中的元素,一次一個。實際上,我們已經使用了很多迭代器:for循環給你一個迭代器。當你想在其他時候使用迭代器時,你必須選擇什麼樣的迭代器:

  • .iter() 引用的迭代器
  • .iter_mut() 可變引用的迭代器
  • .into_iter() 值的迭代器(不是引用)

for循環其實只是一個擁有值的迭代器。這就是為什麼可以讓它變得可變,然後你可以在使用的時候改變值。

我們可以這樣使用迭代器。

fn main() {
    let vector1 = vec![1, 2, 3]; // we will use .iter() and .into_iter() on this one
    let vector1_a = vector1.iter().map(|x| x + 1).collect::<Vec<i32>>();
    let vector1_b = vector1.into_iter().map(|x| x * 10).collect::<Vec<i32>>();

    let mut vector2 = vec![10, 20, 30]; // we will use .iter_mut() on this one
    vector2.iter_mut().for_each(|x| *x +=100);

    println!("{:?}", vector1_a);
    println!("{:?}", vector2);
    println!("{:?}", vector1_b);
}

這個將打印:

[2, 3, 4]
[110, 120, 130]
[10, 20, 30]

前兩個我們用了一個叫.map()的方法。這個方法可以讓你對每一個元素做一些事情,然後把它傳遞下去。最後我們用的是一個叫.for_each()的方法。這個方法只是讓你對每一個元素做一些事情。.iter_mut()加上for_each()基本上就是一個for的循環。在每一個方法裡面,我們可以給每一個元素起一個名字(我們剛才叫它 x),然後用它來改變它。這些被稱為閉包,我們將在下一節學習它們。

讓我們再來看看它們,一次一個。

首先我們用.iter()vector1進行引用。我們給每個元素都加了1,並使其成為一個新的Vec。vector1還活著,因為我們只用了引用:我們沒有按值取。現在我們有 vector1,還有一個新的 Vec 叫 vector1_a。因為.map()只是傳遞了它,所以我們需要使用.collect()把它變成一個Vec

然後我們用into_itervector1中按值得到一個迭代器。這樣就破壞了vector1,因為這就是into_iter()的作用。所以我們做了vector1_b之後,就不能再使用vector1了。

最後我們在vector2上使用.iter_mut()。它是可變的,所以我們不需要使用.collect()來創建一個新的Vec。相反,我們用可變引用改變同一Vec中的值。所以vector2仍然存在。因為我們不需要一個新的Vec,我們使用for_each:它就像一個for循環。

迭代器如何工作

迭代器的工作原理是使用一個叫做 .next() 的方法,它給出一個 Option。當你使用迭代器時,Rust會一遍又一遍地調用next()。如果得到 Some,它就會繼續前進。如果得到 None,它就停止。

你還記得 assert_eq! 宏嗎?在文檔中,你經常看到它。這裡它展示了迭代器的工作原理。

fn main() {
    let my_vec = vec!['a', 'b', '거', '柳']; // Just a regular Vec

    let mut my_vec_iter = my_vec.iter(); // This is an Iterator type now, but we haven't called it yet

    assert_eq!(my_vec_iter.next(), Some(&'a'));  // Call the first item with .next()
    assert_eq!(my_vec_iter.next(), Some(&'b'));  // Call the next
    assert_eq!(my_vec_iter.next(), Some(&'거')); // Again
    assert_eq!(my_vec_iter.next(), Some(&'柳')); // Again
    assert_eq!(my_vec_iter.next(), None);        // Nothing is left: just None
    assert_eq!(my_vec_iter.next(), None);        // You can keep calling .next() but it will always be None
}

為自己的struct或enum實現Iterator並不難。首先我們創建一個書庫,想一想。

#[derive(Debug)] // we want to print it with {:?}
struct Library {
    library_type: LibraryType, // this is our enum
    books: Vec<String>, // list of books
}

#[derive(Debug)]
enum LibraryType { // libraries can be city libraries or country libraries
    City,
    Country,
}

impl Library {
    fn add_book(&mut self, book: &str) { // we use add_book to add new books
        self.books.push(book.to_string()); // we take a &str and turn it into a String, then add it to the Vec
    }

    fn new() -> Self { // this creates a new Library
        Self {
            library_type: LibraryType::City, // most are in the city so we'll choose City
                                             // most of the time
            books: Vec::new(),
        }
    }
}

fn main() {
    let mut my_library = Library::new(); // make a new library
    my_library.add_book("The Doom of the Darksword"); // add some books
    my_library.add_book("Demian - die Geschichte einer Jugend");
    my_library.add_book("구운몽");
    my_library.add_book("吾輩は貓である");

    println!("{:?}", my_library.books); // we can print our list of books
}

這很好用。現在我們想為庫實現Iterator,這樣我們就可以在for循環中使用它。現在如果我們嘗試 for 循環,它就無法工作。

#![allow(unused)]
fn main() {
for item in my_library {
    println!("{}", item); // ⚠️
}
}

它說:

error[E0277]: `Library` is not an iterator
  --> src\main.rs:47:16
   |
47 |    for item in my_library {
   |                ^^^^^^^^^^ `Library` is not an iterator
   |
   = help: the trait `std::iter::Iterator` is not implemented for `Library`
   = note: required by `std::iter::IntoIterator::into_iter`

但是我們可以用impl Iterator for Library把庫做成迭代器。Iteratortrait的信息在標準庫中。https://doc.rust-lang.org/std/iter/trait.Iterator.html

在頁面的左上方寫著:Associated Types: ItemRequired Methods: next。"關聯類型"的意思是 "一起使用的類型"。我們的關聯類型將是String,因為我們希望迭代器給我們提供String。

在頁面中,它有一個看起來像這樣的例子。

// an iterator which alternates between Some and None
struct Alternate {
    state: i32,
}

impl Iterator for Alternate {
    type Item = i32;

    fn next(&mut self) -> Option<i32> {
        let val = self.state;
        self.state = self.state + 1;

        // if it's even, Some(i32), else None
        if val % 2 == 0 {
            Some(val)
        } else {
            None
        }
    }
}

fn main() {}

你可以看到impl Iterator for Alternate下面寫著type Item = i32。這就是關聯類型。我們的迭代器將針對我們的書籍列表,這是一個Vec<String>。當我們調用next的時候。 它將給我們一個String。所以我們就寫type Item = String;。這就是關聯項。

為了實現 Iterator,你需要寫 fn next() 函數。這是你決定迭代器應該做什麼的地方。對於我們的 Library,我們首先希望它給我們最後一本書。所以我們將match.pop()一起,如果是Some的話,就把最後一項去掉。我們還想為每個元素打印 "is found!"。現在它看起來像這樣:

#[derive(Debug, Clone)]
struct Library {
    library_type: LibraryType,
    books: Vec<String>,
}

#[derive(Debug, Clone)]
enum LibraryType {
    City,
    Country,
}

impl Library {
    fn add_book(&mut self, book: &str) {
        self.books.push(book.to_string());
    }

    fn new() -> Self {
        Self {
            library_type: LibraryType::City,
            // most of the time
            books: Vec::new(),
        }
    }
}

impl Iterator for Library {
    type Item = String;

    fn next(&mut self) -> Option<String> {
        match self.books.pop() {
            Some(book) => Some(book + " is found!"), // Rust allows String + &str
            None => None,
        }
    }
}

fn main() {
    let mut my_library = Library::new();
    my_library.add_book("The Doom of the Darksword");
    my_library.add_book("Demian - die Geschichte einer Jugend");
    my_library.add_book("구운몽");
    my_library.add_book("吾輩は貓である");

    for item in my_library.clone() { // we can use a for loop now. Give it a clone so Library won't be destroyed
        println!("{}", item);
    }
}

這個打印:

吾輩は貓である is found!
구운몽 is found!
Demian - die Geschichte einer Jugend is found!
The Doom of the Darksword is found!

閉包

閉包就像快速函數,不需要名字。有時它們被稱為lambda。Closures很容易辨識,因為它們使用||而不是()。它們在 Rust 中非常常見,一旦你學會了使用它們,你就會愛不釋手。

你可以將一個閉包綁定到一個變量上,然後當你使用它時,它看起來就像一個函數一樣。

fn main() {
    let my_closure = || println!("This is a closure");
    my_closure();
}

所以這個閉包什麼都不需要:||,並打印一條信息。This is a closure.

||之間我們可以添加輸入變量和類型,就像在()裡面添加函數一樣。

fn main() {
    let my_closure = |x: i32| println!("{}", x);

    my_closure(5);
    my_closure(5+5);
}

這個打印:

5
10

當閉包變得更復雜時,你可以添加一個代碼塊。那就可以隨心所欲的長。

fn main() {
    let my_closure = || {
        let number = 7;
        let other_number = 10;
        println!("The two numbers are {} and {}.", number, other_number);
          // This closure can be as long as we want, just like a function.
    };

    my_closure();
}

但是閉包是特殊的,因為它可以接受閉包之外的變量,即使你只寫||。所以你可以這樣做:

fn main() {
    let number_one = 6;
    let number_two = 10;

    let my_closure = || println!("{}", number_one + number_two);
    my_closure();
}

所以這就打印出了16。你不需要在 || 中放入任何東西,因為它可以直接取 number_onenumber_two 並添加它們。

順便說一下,這就是closure這個名字的由來,因為它們會取變量並將它們 "包圍"在裡面。如果你想很正確的說。

  • 一個||如果不把變量從外面包圍起來 那就是一個 "匿名函數". 匿名的意思是 "沒有名字"。它的工作原理更像一個普通函數。
  • || 從外部包圍變量的函數是 "closure"。它把周圍的變量 "封閉"起來使用。

但是人們經常會把所有的||函數都叫做閉包,所以你不用擔心名字的問題。我們只對任何帶有||的函數說 "closure",但請記住,它可能意味著一個 "匿名函數"。

為什麼要知道這兩者的區別呢?因為匿名函數其實和有名字的函數做的機器代碼是一樣的。它們給人的感覺是 "高層抽象",所以有時候大家會覺得機器代碼會很複雜。但是Rust用它生成的機器碼和普通函數一樣快。

所以我們再來看看閉包能做的一些事情。你也可以這樣做:

fn main() {
    let number_one = 6;
    let number_two = 10;

    let my_closure = |x: i32| println!("{}", number_one + number_two + x);
    my_closure(5);
}

這個閉包取number_onenumber_two。我們還給了它一個新的變量 x,並說 x 是 5.然後它把這三個加在一起打印 21

通常在Rust中,你會在一個方法裡面看到閉包,因為裡面有一個閉包是非常方便的。我們在上一節的 .map().for_each() 中看到了閉包。在那一節中,我們寫了 |x| 來引入迭代器中的下一個元素,這就是一個閉包。

下面再舉一個例子:我們知道,如果unwrap不起作用,可以用unwrap_or方法給出一個值。之前我們寫的是:let fourth = my_vec.get(3).unwrap_or(&0);。但是還有一個unwrap_or_else方法,裡面有一個閉包。所以你可以這樣做:

fn main() {
    let my_vec = vec![8, 9, 10];

    let fourth = my_vec.get(3).unwrap_or_else(|| { // try to unwrap. If it doesn't work,
        if my_vec.get(0).is_some() {               // see if my_vec has something at index [0]
            &my_vec[0]                             // Give the number at index 0 if there is something
        } else {
            &0 // otherwise give a &0
        }
    });

    println!("{}", fourth);
}

當然,閉包也可以很簡單。例如,你可以只寫let fourth = my_vec.get(3).unwrap_or_else(|| &0);。你不需要總是因為有一個閉包就使用{}並寫出複雜的代碼。只要你把||放進去,編譯器就知道你放了你需要的閉包。

最常用的閉包方法可能是.map()。我們再來看看它。下面是一種使用方法。

fn main() {
    let num_vec = vec![2, 4, 6];

    let double_vec = num_vec        // take num_vec
        .iter()                     // iterate over it
        .map(|number| number * 2)   // for each item, multiply by two
        .collect::<Vec<i32>>();     // then make a new Vec from this
    println!("{:?}", double_vec);
}

另一個很好的例子是在.enumerate()之後使用.for_each().enumerate()方法給出一個帶有索引號和元素的迭代器。例如:[10, 9, 8]變成(0, 10), (1, 9), (2, 8)。這裡每個項的類型是(usize, i32)。所以你可以這樣做:

fn main() {
    let num_vec = vec![10, 9, 8];

    num_vec
        .iter()      // iterate over num_vec
        .enumerate() // get (index, number)
        .for_each(|(index, number)| println!("Index number {} has number {}", index, number)); // do something for each one
}

這個將打印:

Index number 0 has number 10
Index number 1 has number 9
Index number 2 has number 8

在這種情況下,我們用for_each代替mapmap是用於對每個元素做一些事情,並將其傳遞出去,而for_each是當你看到每個元素時做一些事情。另外,map不做任何事情,除非你使用collect這樣的方法。

其實,這就是迭代器的有趣之處。如果你嘗試map而不使用collect這樣的方法,編譯器會告訴你,它什麼也不做。它不會崩潰,但編譯器會告訴你,你什麼都沒做。

fn main() {
    let num_vec = vec![10, 9, 8];

    num_vec
        .iter()
        .enumerate()
        .map(|(index, number)| println!("Index number {} has number {}", index, number));

}

它說:

warning: unused `std::iter::Map` that must be used
 --> src\main.rs:4:5
  |
4 | /     num_vec
5 | |         .iter()
6 | |         .enumerate()
7 | |         .map(|(index, number)| println!("Index number {} has number {}", index, number));
  | |_________________________________________________________________________________________^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: iterators are lazy and do nothing unless consumed

這是一個警告,所以這不是一個錯誤:程序運行正常。但是為什麼num_vec沒有任何作用呢?我們可以看看類型就知道了。

  • let num_vec = vec![10, 9, 8]; 現在是一個Vec<i32>

  • .iter() 現在是一個 Iter<i32>。所以它是一個迭代器,其元素為 i32

  • .enumerate()現在是一個Enumerate<Iter<i32>>型。所以它是Enumerate型的Iter型的i32

  • .map()現在是一個Map<Enumerate<Iter<i32>>>的類型。所以它是一個類型Map的類型Enumerate的類型Iter的類型i32

我們所做的只是做了一個越來越複雜的結構。所以這個Map<Enumerate<Iter<i32>>>是一個準備好了的結構,但只有當我們告訴它要做什麼的時候,它才會去做。Rust這樣做是因為它需要保證足夠快。它不想這樣做:

  • 遍歷Vec中所有的i32
  • 然後從迭代器中枚舉出所有的i32
  • 然後將所有列舉的i32映射過來

Rust 只想做一次計算,所以它創建結構並等待。然後,如果我們說.collect::<Vec<i32>>(),它知道該怎麼做,並開始移動。這就是iterators are lazy and do nothing unless consumed的意思。迭代器在你 "消耗"它們(用完它們)之前不會做任何事情。

你甚至可以用.collect()創建像HashMap這樣複雜的東西,所以它非常強大。下面是一個如何將兩個向量放入HashMap的例子。首先我們把兩個向量創建出來,然後我們會對它們使用.into_iter()來得到一個值的迭代器。然後我們使用.zip()方法。這個方法將兩個迭代器連接在一起,就像拉鍊一樣。最後,我們使用.collect()來創建HashMap

下面是代碼。

use std::collections::HashMap;

fn main() {
    let some_numbers = vec![0, 1, 2, 3, 4, 5]; // a Vec<i32>
    let some_words = vec!["zero", "one", "two", "three", "four", "five"]; // a Vec<&str>

    let number_word_hashmap = some_numbers
        .into_iter()                 // now it is an iter
        .zip(some_words.into_iter()) // inside .zip() we put in the other iter. Now they are together.
        .collect::<HashMap<_, _>>();

    println!("For key {} we get {}.", 2, number_word_hashmap.get(&2).unwrap());
}

這個將打印:

For key 2 we get two.

你可以看到,我們寫了 <HashMap<_, _>>,因為這足以讓 Rust 決定 HashMap<i32, &str> 的類型。如果你想寫 .collect::<HashMap<i32, &str>>();也行,也可以這樣寫:

use std::collections::HashMap;

fn main() {
    let some_numbers = vec![0, 1, 2, 3, 4, 5]; // a Vec<i32>
    let some_words = vec!["zero", "one", "two", "three", "four", "five"]; // a Vec<&str>
    let number_word_hashmap: HashMap<_, _> = some_numbers  // Because we tell it the type here...
        .into_iter()
        .zip(some_words.into_iter())
        .collect(); // we don't have to tell it here
}

還有一種方法,就像.enumerate()charchar_indices(). (Indices的意思是 "索引")。你用它的方法是一樣的。假設我們有一個由3位數組成的大字符串。

fn main() {
    let numbers_together = "140399923481800622623218009598281";

    for (index, number) in numbers_together.char_indices() {
        match (index % 3, number) {
            (0..=1, number) => print!("{}", number), // just print the number if there is a remainder
            _ => print!("{}\t", number), // otherwise print the number with a tab space
        }
    }
}

打印140 399 923 481 800 622 623 218 009 598 281

閉包中的|_|

有時你會在一個閉包中看到 |_|。這意味著這個閉包需要一個參數(比如 x),但你不想使用它。所以 |_| 意味著 "好吧,這個閉包需要一個參數,但我不會給它一個名字,因為我不關心它"。

下面是一個錯誤的例子,當你不這樣做的時候。

fn main() {
    let my_vec = vec![8, 9, 10];

    println!("{:?}", my_vec.iter().for_each(|| println!("We didn't use the variables at all"))); // ⚠️
}

Rust說

error[E0593]: closure is expected to take 1 argument, but it takes 0 arguments
  --> src\main.rs:28:36
   |
28 |     println!("{:?}", my_vec.iter().for_each(|| println!("We didn't use the variables at all")));
   |                                    ^^^^^^^^ -- takes 0 arguments
   |                                    |
   |                                    expected closure that takes 1 argument

編譯器其實給你一些幫助。

help: consider changing the closure to take and ignore the expected argument
   |
28 |     println!("{:?}", my_vec.iter().for_each(|_| println!("We didn't use the variables at all")));

這是很好的建議。如果你把||改成|_|就可以了。

閉包和迭代器的有用方法

一旦你熟悉了閉包,Rust就會成為一種非常有趣的語言。有了閉包,你可以將方法互相鏈接起來,用很少的代碼做很多事情。下面是一些我們還沒有見過的閉包和使用閉包的方法。

.filter(): 這可以讓你在迭代器中保留你想保留的元素。讓我們過濾一年中的月份。

fn main() {
    let months = vec!["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];

    let filtered_months = months
        .into_iter()                         // make an iter
        .filter(|month| month.len() < 5)     // We don't want months more than 5 bytes in length.
                                             // We know that each letter is one byte so .len() is fine
        .filter(|month| month.contains("u")) // Also we only like months with the letter u
        .collect::<Vec<&str>>();

    println!("{:?}", filtered_months);
}

這個打印["June", "July"]

.filter_map(). 這個叫做filter_map(),因為它做了.filter().map()。閉包必須返回一個 Option<T>,然後對每個Option, 如果是 Some, filter_map()將取出它的值。所以比如說你.filter_map()一個vec![Some(2), None, Some(3)],它就會返回[2, 3]

我們將用一個Company結構體來寫一個例子。每個公司都有一個name,所以這個字段是String,但是CEO可能最近已經辭職了。所以ceo字段是Option<String>。我們會.filter_map()過一些公司,只保留CEO名字。

struct Company {
    name: String,
    ceo: Option<String>,
}

impl Company {
    fn new(name: &str, ceo: &str) -> Self {
        let ceo = match ceo {
            "" => None,
            name => Some(name.to_string()),
        }; // ceo is decided, so now we return Self
        Self {
            name: name.to_string(),
            ceo,
        }
    }

    fn get_ceo(&self) -> Option<String> {
        self.ceo.clone() // Just returns a clone of the CEO (struct is not Copy)
    }
}

fn main() {
    let company_vec = vec![
        Company::new("Umbrella Corporation", "Unknown"),
        Company::new("Ovintiv", "Doug Suttles"),
        Company::new("The Red-Headed League", ""),
        Company::new("Stark Enterprises", ""),
    ];

    let all_the_ceos = company_vec
        .into_iter()
        .filter_map(|company| company.get_ceo()) // filter_map needs Option<T>
        .collect::<Vec<String>>();

    println!("{:?}", all_the_ceos);
}

這就打印出了["Unknown", "Doug Suttles"]

既然 .filter_map() 需要 Option,那麼 Result 呢?沒問題:有一個叫做 .ok() 的方法,可以把 Result 變成 Option。之所以叫.ok(),是因為它能發送的只是Ok的結果(Err的信息沒有了)。你記得OptionOption<T>,而ResultResult<T, E>,同時有OkErr的信息。所以當你使用.ok()時,任何Err的信息都會丟失,變成None

使用 .parse() 是一個很簡單的例子,我們嘗試解析一些用戶輸入。.parse()在這裡接受一個&str,並試圖把它變成一個f32。它返回一個 Result,但我們使用的是 filter_map(),所以我們只需拋出錯誤。Err的任何內容都會變成None,並被.filter_map()過濾掉。

fn main() {
    let user_input = vec!["8.9", "Nine point nine five", "8.0", "7.6", "eleventy-twelve"];

    let actual_numbers = user_input
        .into_iter()
        .filter_map(|input| input.parse::<f32>().ok())
        .collect::<Vec<f32>>();

    println!("{:?}", actual_numbers);
}

將打印: [8.9, 8.0, 7.6]

.ok()相對的是.ok_or()ok_or_else()。這樣就把Option變成了Result。之所以叫.ok_or(),是因為Result給出了一個OkErr,所以你必須讓它知道Err的值是多少。這是因為None中的Option沒有任何信息。另外,你現在可以看到,這些方法名稱中的else部分意味著它有一個閉包。

我們可以把我們的OptionCompany結構中取出來,然後這樣把它變成一個Result。對於長期的錯誤處理,最好是創建自己的錯誤類型。 但是現在我們只是給它一個錯誤信息,所以它就變成了Result<String, &str>

// Everything before main() is exactly the same
struct Company {
    name: String,
    ceo: Option<String>,
}

impl Company {
    fn new(name: &str, ceo: &str) -> Self {
        let ceo = match ceo {
            "" => None,
            name => Some(name.to_string()),
        };
        Self {
            name: name.to_string(),
            ceo,
        }
    }

    fn get_ceo(&self) -> Option<String> {
        self.ceo.clone()
    }
}

fn main() {
    let company_vec = vec![
        Company::new("Umbrella Corporation", "Unknown"),
        Company::new("Ovintiv", "Doug Suttles"),
        Company::new("The Red-Headed League", ""),
        Company::new("Stark Enterprises", ""),
    ];

    let mut results_vec = vec![]; // Pretend we need to gather error results too

    company_vec
        .iter()
        .for_each(|company| results_vec.push(company.get_ceo().ok_or("No CEO found")));

    for item in results_vec {
        println!("{:?}", item);
    }
}

這行是最大的變化:

#![allow(unused)]
fn main() {
// 🚧
.for_each(|company| results_vec.push(company.get_ceo().ok_or("No CEO found")));
}

它的意思是:"每家公司,用get_ceo(). 如果你得到了,那就把Ok裡面的數值傳給你。如果沒有,就在Err裡面傳遞 "沒有找到CEO"。然後把這個推到vec裡。"

所以當我們打印results_vec的時候,就會得到這樣的結果。

Ok("Unknown")
Ok("Doug Suttles")
Err("No CEO found")
Err("No CEO found")

所以現在我們有了所有四個條目。現在讓我們使用 .ok_or_else(),這樣我們就可以使用一個閉包,並得到一個更好的錯誤信息。現在我們有空間使用format!來創建一個String,並將公司名稱放在其中。然後我們返回String

// Everything before main() is exactly the same
struct Company {
    name: String,
    ceo: Option<String>,
}

impl Company {
    fn new(name: &str, ceo: &str) -> Self {
        let ceo = match ceo {
            "" => None,
            name => Some(name.to_string()),
        };
        Self {
            name: name.to_string(),
            ceo,
        }
    }

    fn get_ceo(&self) -> Option<String> {
        self.ceo.clone()
    }
}

fn main() {
    let company_vec = vec![
        Company::new("Umbrella Corporation", "Unknown"),
        Company::new("Ovintiv", "Doug Suttles"),
        Company::new("The Red-Headed League", ""),
        Company::new("Stark Enterprises", ""),
    ];

    let mut results_vec = vec![];

    company_vec.iter().for_each(|company| {
        results_vec.push(company.get_ceo().ok_or_else(|| {
            let err_message = format!("No CEO found for {}", company.name);
            err_message
        }))
    });

    for item in results_vec {
        println!("{:?}", item);
    }
}

這樣一來,我們就有了。

Ok("Unknown")
Ok("Doug Suttles")
Err("No CEO found for The Red-Headed League")
Err("No CEO found for Stark Enterprises")

.and_then()是一個有用的方法,它接收一個Option,然後讓你對它的值做一些事情,並把它傳遞出去。所以它的輸入是一個 Option,輸出也是一個 Option。這有點像一個安全的 "解包,然後做一些事情,然後再包"。

一個簡單的例子是,我們使用 .get() 從一個 vec 中得到一個數字,因為它返回一個 Option。現在我們可以把它傳給 and_then(),如果它是 Some,我們可以對它做一些數學運算。如果是None,那麼None就會被傳遞過去。

fn main() {
    let new_vec = vec![8, 9, 0]; // just a vec with numbers

    let number_to_add = 5;       // use this in the math later
    let mut empty_vec = vec![];  // results go in here


    for index in 0..5 {
        empty_vec.push(
            new_vec
               .get(index)
                .and_then(|number| Some(number + 1))
                .and_then(|number| Some(number + number_to_add))
        );
    }
    println!("{:?}", empty_vec);
}

這就打印出了[Some(14), Some(15), Some(6), None, None]。你可以看到None並沒有被過濾掉,只是傳遞了。

.and()有點像Optionbool。你可以匹配很多個Option,如果它們都是Some,那麼它會給出最後一個。而如果其中一個是None,那麼就會給出None

首先這裡有一個bool的例子來幫助想象。你可以看到,如果你用的是&&(和),哪怕是一個false,也會讓一切false

fn main() {
    let one = true;
    let two = false;
    let three = true;
    let four = true;

    println!("{}", one && three); // prints true
    println!("{}", one && two && three && four); // prints false
}

現在這裡的.and()也是一樣的。想象一下,我們做了五次操作,並把結果放在一個Vec<Option<&str>>中。如果我們得到一個值,我們就把Some("success!")推到Vec中。然後我們再做兩次這樣的操作。之後我們用.and()每次只顯示得到Some的索引。

fn main() {
    let first_try = vec![Some("success!"), None, Some("success!"), Some("success!"), None];
    let second_try = vec![None, Some("success!"), Some("success!"), Some("success!"), Some("success!")];
    let third_try = vec![Some("success!"), Some("success!"), Some("success!"), Some("success!"), None];

    for i in 0..first_try.len() {
        println!("{:?}", first_try[i].and(second_try[i]).and(third_try[i]));
    }
}

這個打印:

None
None
Some("success!")
Some("success!")
None

第一個(索引0)是None,因為在second_try中有一個None為索引0。第二個是None,因為在first_try中有一個None。其次是Some("success!"),因為first_trysecond trythird_try中沒有None

.any().all()在迭代器中非常容易使用。它們根據你的輸入返回一個bool。在這個例子中,我們做了一個非常大的vec(大約20000個元素),包含了從'a''働'的所有字符。然後我們創建一個函數來檢查是否有字符在其中。

接下來我們創建一個更小的vec,問它是否都是字母(用.is_alphabetic()方法)。然後我們問它是不是所有的字符都小於韓文字符'행'

還要注意放一個參照物,因為.iter()給了一個參照物,你需要一個&和另一個&進行比較。

fn in_char_vec(char_vec: &Vec<char>, check: char) {
    println!("Is {} inside? {}", check, char_vec.iter().any(|&char| char == check));
}

fn main() {
    let char_vec = ('a'..'働').collect::<Vec<char>>();
    in_char_vec(&char_vec, 'i');
    in_char_vec(&char_vec, '뷁');
    in_char_vec(&char_vec, '鑿');

    let smaller_vec = ('A'..'z').collect::<Vec<char>>();
    println!("All alphabetic? {}", smaller_vec.iter().all(|&x| x.is_alphabetic()));
    println!("All less than the character 행? {}", smaller_vec.iter().all(|&x| x < '행'));
}

這個打印:

Is i inside? true
Is 뷁 inside? false
Is 鑿 inside? false
All alphabetic? false
All less than the character 행? true

順便說一下,.any()只檢查到一個匹配的元素,然後就停止了。如果它已經找到了一個匹配項,它不會檢查所有的元素。如果您要在 Vec 上使用 .any(),最好把可能匹配的元素推到前面。或者你可以在 .iter() 之後使用 .rev() 來反向迭代。這裡有一個這樣的vec。

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);
}

所以這個Vec有1000個6,後面還有一個5。我們假設我們要用.any()來看看它是否包含5。首先讓我們確定.rev()是有效的。記住,一個Iterator總是有.next(),讓你每次都檢查它的工作。

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);

    let mut iterator = big_vec.iter().rev();
    println!("{:?}", iterator.next());
    println!("{:?}", iterator.next());
}

它的打印。

Some(5)
Some(6)

我們是對的:有一個Some(5),然後1000個Some(6)開始。所以我們可以這樣寫。

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);

    println!("{:?}", big_vec.iter().rev().any(|&number| number == 5));
}

而且因為是.rev(),所以它只調用.next()一次就停止了。如果我們不用.rev(),那麼它將調用.next() 1001次才停止。這段代碼顯示了它。

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);

    let mut counter = 0; // Start counting
    let mut big_iter = big_vec.into_iter(); // Make it an Iterator

    loop {
        counter +=1;
        if big_iter.next() == Some(5) { // Keep calling .next() until we get Some(5)
            break;
        }
    }
    println!("Final counter is: {}", counter);
}

這將打印出 Final counter is: 1001,所以我們知道它必須調用 .next() 1001 次才能找到 5。

.find() 告訴你一個迭代器是否有東西,而 .position() 告訴你它在哪裡。.find().any()不同,因為它返回一個Option,裡面有值(或None)。同時,.position()也是一個帶有位置號的Option,或None。換句話說

  • .find(): "我儘量幫你拿"
  • .position():"我幫你找找看在哪裡"

下面是一個簡單的例子。

fn main() {
    let num_vec = vec![10, 20, 30, 40, 50, 60, 70, 80, 90, 100];

    println!("{:?}", num_vec.iter().find(|&number| number % 3 == 0)); // find takes a reference, so we give it &number
    println!("{:?}", num_vec.iter().find(|&number| number * 2 == 30));

    println!("{:?}", num_vec.iter().position(|&number| number % 3 == 0));
    println!("{:?}", num_vec.iter().position(|&number| number * 2 == 30));

}

這個打印:

Some(30) // This is the number itself
None // No number inside times 2 == 30
Some(2) // This is the position
None

使用 .cycle() 你可以創建一個永遠循環的迭代器。這種類型的迭代器與 .zip() 很好地結合在一起,可以創建新的東西,就像這個例子,它創建了一個 Vec<(i32, &str)>

fn main() {
    let even_odd = vec!["even", "odd"];

    let even_odd_vec = (0..6)
        .zip(even_odd.into_iter().cycle())
        .collect::<Vec<(i32, &str)>>();
    println!("{:?}", even_odd_vec);
}

所以,即使.cycle()可能永遠不會結束,但當把它們壓縮在一起時,另一個迭代器只運行了6次。 也就是說,.cycle()所做的迭代器不會再被.next()調用,所以六次之後就完成了。輸出的結果是

[(0, "even"), (1, "odd"), (2, "even"), (3, "odd"), (4, "even"), (5, "odd")]

類似的事情也可以用一個沒有結尾的範圍來完成。如果你寫0..,那麼你就創建了一個永不停止的範圍。你可以很容易地使用這個方法。

fn main() {
    let ten_chars = ('a'..).take(10).collect::<Vec<char>>();
    let skip_then_ten_chars = ('a'..).skip(1300).take(10).collect::<Vec<char>>();

    println!("{:?}", ten_chars);
    println!("{:?}", skip_then_ten_chars);
}

兩者都是打印十個字符,但第二個跳過1300位,打印的是亞美尼亞語的十個字母。

['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
['յ', 'ն', 'շ', 'ո', 'չ', 'պ', 'ջ', 'ռ', 'ս', 'վ']

另一種流行的方法叫做.fold()。這個方法經常用於將迭代器中的元素加在一起,但你也可以做更多的事情。它與.for_each()有些類似。在 .fold() 中,你首先添加一個起始值 (如果你是把元素加在一起,那麼就是 0),然後是一個逗號,然後是閉包。結尾給你兩個元素:到目前為止的總數,和下一個元素。首先這裡有一個簡單的例子,顯示.fold()將元素加在一起。

fn main() {
    let some_numbers = vec![9, 6, 9, 10, 11];

    println!("{}", some_numbers
        .iter()
        .fold(0, |total_so_far, next_number| total_so_far + next_number)
    );
}

所以,在第1步中,它從0開始,再加上下一個數字:9。

  • 第1步,從0開始,加上下一個數字9
  • 然後把9加上6: 15。
  • 然後把15加上9: 24。
  • 然後取24,再加上10: 34。
  • 最後取34,再加上11: 45。所以它的打印結果是45.

但是你不需要只用它來添加東西。下面是一個例子,我們在每一個字符上加一個'-',就會變成String

fn main() {
    let a_string = "I don't have any dashes in me.";

    println!(
        "{}",
        a_string
            .chars() // Now it's an iterator
            .fold("-".to_string(), |mut string_so_far, next_char| { // Start with a String "-". Bring it in as mutable each time along with the next char
                string_so_far.push(next_char); // Push the char on, then '-'
                string_so_far.push('-');
                string_so_far} // Don't forget to pass it on to the next loop
            ));
}

這個打印:

-I- -d-o-n-'-t- -h-a-v-e- -a-n-y- -d-a-s-h-e-s- -i-n- -m-e-.-

還有很多其他方便的方法,比如

  • .take_while(),只要得到true,就會帶入一個迭代器(例如take while x > 5)
  • .cloned(),它在迭代器內做了一個克隆。這將一個引用變成了一個值。
  • .by_ref(),它使迭代器取一個引用。這很好的保證了你使用Vec或類似的方法來創建迭代器後可以使用它。
  • 許多其他的_while方法:.skip_while().map_while()等等。
  • .sum():就是把所有的東西加在一起。

.chunks().windows()是將矢量切割成你想要的尺寸的兩種方法。你把你想要的尺寸放在括號裡。比如說你有一個有10個元素的矢量,你想要一個3的尺寸,它的工作原理是這樣的。

  • .chunks()會給你4個切片: [0, 1, 2], 然後是[3, 4, 5], 然後是[6, 7, 8], 最後是[9]. 所以它會嘗試用三個元素創建一個切片,但如果它沒有三個元素,那麼它就不會崩潰。它只會給你剩下的東西。

  • .windows()會先給你一個[0, 1, 2]的切片。然後它將移過一片,給你[1, 2, 3]。它將一直這樣做,直到最後到達3的最後一片,然後停止。

所以讓我們在一個簡單的數字向量上使用它們。它看起來像這樣:

fn main() {
    let num_vec = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0];

    for chunk in num_vec.chunks(3) {
        println!("{:?}", chunk);
    }
    println!();
    for window in num_vec.windows(3) {
        println!("{:?}", window);
    }
}

這個打印:

[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
[0]

[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
[4, 5, 6]
[5, 6, 7]
[6, 7, 8]
[7, 8, 9]
[8, 9, 0]

順便說一下,如果你什麼都不給它,.chunks()會崩潰。你可以為一個只有一項的向量寫.chunks(1000),但你不能為任何長度為0的東西寫.chunks()。 如果你點擊[src],你可以在函數中看到這一點,因為它說assert!(chunk_size != 0);

.match_indices() 讓你把 String&str 裡面所有符合你的輸入的東西都提取出來,並給你索引。它與 .enumerate() 類似,因為它返回一個包含兩個元素的元組。

fn main() {
    let rules = "Rule number 1: No fighting. Rule number 2: Go to bed at 8 pm. Rule number 3: Wake up at 6 am.";
    let rule_locations = rules.match_indices("Rule").collect::<Vec<(_, _)>>(); // This is Vec<usize, &str> but we just tell Rust to do it
    println!("{:?}", rule_locations);
}

這個打印:

[(0, "Rule"), (28, "Rule"), (62, "Rule")]

.peekable() 讓你創建一個迭代器,在那裡你可以看到 (窺視) 下一個元素。它就像調用 .next() (它給出了一個 Option),除了迭代器不會移動,所以你可以隨意使用它。實際上,你可以把peekable看成是 "可停止"的,因為你可以想停多久就停多久。下面是一個例子,我們對每個元素都使用.peek()三次。我們可以永遠使用.peek(),直到我們使用.next()移動到下一個元素。

fn main() {
    let just_numbers = vec![1, 5, 100];
    let mut number_iter = just_numbers.iter().peekable(); // This actually creates a type of iterator called Peekable

    for _ in 0..3 {
        println!("I love the number {}", number_iter.peek().unwrap());
        println!("I really love the number {}", number_iter.peek().unwrap());
        println!("{} is such a nice number", number_iter.peek().unwrap());
        number_iter.next();
    }
}

這個打印:

I love the number 1
I really love the number 1
1 is such a nice number
I love the number 5
I really love the number 5
5 is such a nice number
I love the number 100
I really love the number 100
100 is such a nice number

下面是另一個例子,我們使用.peek()對一個元素進行匹配。使用完後,我們調用.next()

fn main() {
    let locations = vec![
        ("Nevis", 25),
        ("Taber", 8428),
        ("Markerville", 45),
        ("Cardston", 3585),
    ];
    let mut location_iter = locations.iter().peekable();
    while location_iter.peek().is_some() {
        match location_iter.peek() {
            Some((name, number)) if *number < 100 => { // .peek() gives us a reference so we need *
                println!("Found a hamlet: {} with {} people", name, number)
            }
            Some((name, number)) => println!("Found a town: {} with {} people", name, number),
            None => break,
        }
        location_iter.next();
    }
}

這個打印:

Found a hamlet: Nevis with 25 people
Found a town: Taber with 8428 people
Found a hamlet: Markerville with 45 people
Found a town: Cardston with 3585 people

最後,這裡有一個例子,我們也使用.match_indices()。在這個例子中,我們根據&str中的空格數,將名字放入struct中。

#[derive(Debug)]
struct Names {
    one_word: Vec<String>,
    two_words: Vec<String>,
    three_words: Vec<String>,
}

fn main() {
    let vec_of_names = vec![
        "Caesar",
        "Frodo Baggins",
        "Bilbo Baggins",
        "Jean-Luc Picard",
        "Data",
        "Rand Al'Thor",
        "Paul Atreides",
        "Barack Hussein Obama",
        "Bill Jefferson Clinton",
    ];

    let mut iter_of_names = vec_of_names.iter().peekable();

    let mut all_names = Names { // start an empty Names struct
        one_word: vec![],
        two_words: vec![],
        three_words: vec![],
    };

    while iter_of_names.peek().is_some() {
        let next_item = iter_of_names.next().unwrap(); // We can use .unwrap() because we know it is Some
        match next_item.match_indices(' ').collect::<Vec<_>>().len() { // Create a quick vec using .match_indices and check the length
            0 => all_names.one_word.push(next_item.to_string()),
            1 => all_names.two_words.push(next_item.to_string()),
            _ => all_names.three_words.push(next_item.to_string()),
        }
    }

    println!("{:?}", all_names);
}

這將打印:

Names { one_word: ["Caesar", "Data"], two_words: ["Frodo Baggins", "Bilbo Baggins", "Jean-Luc Picard", "Rand Al\'Thor", "Paul Atreides"], three_words:
["Barack Hussein Obama", "Bill Jefferson Clinton"] }

dbg! 宏和.inspect

dbg!是一個非常有用的宏,可以快速打印信息。它是 println! 的一個很好的替代品,因為它的輸入速度更快,提供的信息更多。

fn main() {
    let my_number = 8;
    dbg!(my_number);
}

這樣就可以打印出[src\main.rs:4] my_number = 8

但實際上,你可以把dbg!放在其他很多地方,甚至可以把代碼包在裡面。比如看這段代碼。

fn main() {
    let mut my_number = 9;
    my_number += 10;

    let new_vec = vec![8, 9, 10];

    let double_vec = new_vec.iter().map(|x| x * 2).collect::<Vec<i32>>();
}

這段代碼創建了一個新的可變數字,並改變了它。然後創建一個vec,並使用itermap以及collect創建一個新的vec。在這段代碼中,我們幾乎可以把dbg!放在任何地方。dbg!問編譯器:"此刻你在做什麼?",然後告訴你:

fn main() {
    let mut my_number = dbg!(9);
    dbg!(my_number += 10);

    let new_vec = dbg!(vec![8, 9, 10]);

    let double_vec = dbg!(new_vec.iter().map(|x| x * 2).collect::<Vec<i32>>());

    dbg!(double_vec);
}

所以這個打印:

[src\main.rs:3] 9 = 9

和:

[src\main.rs:4] my_number += 10 = ()

和:

[src\main.rs:6] vec![8, 9, 10] = [
    8,
    9,
    10,
]

而這個,甚至可以顯示出表達式的值。

[src\main.rs:8] new_vec.iter().map(|x| x * 2).collect::<Vec<i32>>() = [
    16,
    18,
    20,
]

和:

[src\main.rs:10] double_vec = [
    16,
    18,
    20,
]

.inspectdbg! 有點類似,就像在迭代器中使用map一樣使用它。它給了你迭代項,你可以打印它或者做任何你想做的事情。例如,我們再看看我們的 double_vec

fn main() {
    let new_vec = vec![8, 9, 10];

    let double_vec = new_vec
        .iter()
        .map(|x| x * 2)
        .collect::<Vec<i32>>();
}

我們想知道更多關於代碼的信息。所以我們在兩個地方添加inspect()

fn main() {
    let new_vec = vec![8, 9, 10];

    let double_vec = new_vec
        .iter()
        .inspect(|first_item| println!("The item is: {}", first_item))
        .map(|x| x * 2)
        .inspect(|next_item| println!("Then it is: {}", next_item))
        .collect::<Vec<i32>>();
}

這個打印:

The item is: 8
Then it is: 16
The item is: 9
Then it is: 18
The item is: 10
Then it is: 20

而且因為.inspect採取的是封閉式,所以我們可以隨意寫。

fn main() {
    let new_vec = vec![8, 9, 10];

    let double_vec = new_vec
        .iter()
        .inspect(|first_item| {
            println!("The item is: {}", first_item);
            match **first_item % 2 { // first item is a &&i32 so we use **
                0 => println!("It is even."),
                _ => println!("It is odd."),
            }
            println!("In binary it is {:b}.", first_item);
        })
        .map(|x| x * 2)
        .collect::<Vec<i32>>();
}

這個打印:

The item is: 8
It is even.
In binary it is 1000.
The item is: 9
It is odd.
In binary it is 1001.
The item is: 10
It is even.
In binary it is 1010.

&str的類型

&str的類型不止一種。我們有。

  • 字符串: 當你寫let my_str = "I am a &str"的時候,你就會產生這些字符。它們在整個程序中持續存在,因為它們是直接寫進二進制中的,它們的類型是 &'static str'的意思是它的生命期,字符串字元有一個叫static的生命期。
  • 借用str:這是常規的 &str 形式,沒有 static 生命期。如果你創建了一個String,並得到了它的引用,當你需要它時,Rust會把它轉換為&str。比如說
fn prints_str(my_str: &str) { // it can use &String like a &str
    println!("{}", my_str);
}

fn main() {
    let my_string = String::from("I am a string");
    prints_str(&my_string); // we give prints_str a &String
}

那麼什麼是lifetime呢?我們現在就來瞭解一下。

生命期

生命期的意思是 "變量的生命期有多長"。你只需要考慮引用的生命期。這是因為引用的生命期不能比它們來自的對象更長。例如,這個函數就不能用。

fn returns_reference() -> &str {
    let my_string = String::from("I am a string");
    &my_string // ⚠️
}

fn main() {}

問題是my_string只存在於returns_reference中。我們試圖返回 &my_string,但是 &my_string 不能沒有 my_string。所以編譯器說不行。

這個代碼也不行。

fn returns_str() -> &str {
    let my_string = String::from("I am a string");
    "I am a str" // ⚠️
}

fn main() {
    let my_str = returns_str();
    println!("{}", my_str);
}

但幾乎是成功的。編譯器說:

error[E0106]: missing lifetime specifier
 --> src\main.rs:6:21
  |
6 | fn returns_str() -> &str {
  |                     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
6 | fn returns_str() -> &'static str {
  |                     ^^^^^^^^

missing lifetime specifier的意思是,我們需要加一個'的生命期。然後說它contains a borrowed value, but there is no value for it to be borrowed from。也就是說,I am a str不是借來的。它寫&'static str就說consider using the 'static lifetime。所以它認為我們應該嘗試說這是一個字符串的文字。

現在它工作了。

fn returns_str() -> &'static str {
    let my_string = String::from("I am a string");
    "I am a str"
}

fn main() {
    let my_str = returns_str();
    println!("{}", my_str);
}

這是因為我們返回了一個 &str,生命期為 static。同時,my_string只能以String的形式返回:我們不能返回對它的引用,因為它將在下一行死亡。

所以現在fn returns_str() -> &'static str告訴Rust, "別擔心,我們只會返回一個字符串字面量". 字符串字面量在整個程序中都是有效的,所以Rust很高興。你會注意到,這與泛型類似。當我們告訴編譯器類似 <T: Display> 的東西時,我們承諾我們將只使用實現了 Display 的輸入。生命期也類似:我們並沒有改變任何變量的生命期。我們只是告訴編譯器輸入的生命期是多少。

但是'static並不是唯一的生命期。實際上,每個變量都有一個生命期,但通常我們不必寫出來。編譯器很聰明,一般都能自己算出來。只有在編譯器不知道的時候,我們才需要寫出生命期。

下面是另一個生命期的例子。想象一下,我們想創建一個City結構,並給它一個&str的名字。我們可能想這樣做,因為這樣做的性能比用String快。所以我們這樣寫,但還不能用。

#[derive(Debug)]
struct City {
    name: &str, // ⚠️
    date_founded: u32,
}

fn main() {
    let my_city = City {
        name: "Ichinomiya",
        date_founded: 1921,
    };
}

編譯器說:

error[E0106]: missing lifetime specifier
 --> src\main.rs:3:11
  |
3 |     name: &str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
2 | struct City<'a> {
3 |     name: &'a str,
  |

Rust 需要 &str 的生命期,因為 &str 是一個引用。如果name指向的值被丟棄了會怎樣?那就不安全了。

'static呢,能用嗎?我們以前用過。我們試試吧。

#[derive(Debug)]
struct City {
    name: &'static str, // change &str to &'static str
    date_founded: u32,
}

fn main() {
    let my_city = City {
        name: "Ichinomiya",
        date_founded: 1921,
    };

    println!("{} was founded in {}", my_city.name, my_city.date_founded);
}

好的,這就可以了。也許這就是你想要的結構。但是,請注意,我們只能接受 "字符串字面量",所以不能接受對其他東西的引用。所以這將無法工作。

#[derive(Debug)]
struct City {
    name: &'static str, // must live for the whole program
    date_founded: u32,
}

fn main() {
    let city_names = vec!["Ichinomiya".to_string(), "Kurume".to_string()]; // city_names does not live for the whole program

    let my_city = City {
        name: &city_names[0], // ⚠️ This is a &str, but not a &'static str. It is a reference to a value inside city_names
        date_founded: 1921,
    };

    println!("{} was founded in {}", my_city.name, my_city.date_founded);
}

編譯器說:

error[E0597]: `city_names` does not live long enough
  --> src\main.rs:12:16
   |
12 |         name: &city_names[0],
   |                ^^^^^^^^^^
   |                |
   |                borrowed value does not live long enough
   |                requires that `city_names` is borrowed for `'static`
...
18 | }
   | - `city_names` dropped here while still borrowed

這一點很重要,因為我們給它的引用其實已經夠長壽了。但是我們承諾只給它一個&'static str,這就是問題所在。

所以現在我們就試試之前編譯器的建議。它說嘗試寫struct City<'a>name: &'a str。這就意味著,只有當name活到City一樣壽命的情況下,它才會接受name的引用。

#[derive(Debug)]
struct City<'a> { // City has lifetime 'a
    name: &'a str, // and name also has lifetime 'a.
    date_founded: u32,
}

fn main() {
    let city_names = vec!["Ichinomiya".to_string(), "Kurume".to_string()];

    let my_city = City {
        name: &city_names[0],
        date_founded: 1921,
    };

    println!("{} was founded in {}", my_city.name, my_city.date_founded);
}

另外記住,如果你願意,你可以寫任何東西來代替'a。這也和泛型類似,我們寫TU,但實際上可以寫任何東西。

#[derive(Debug)]
struct City<'city> { // The lifetime is now called 'city
    name: &'city str, // and name has the 'city lifetime
    date_founded: u32,
}

fn main() {}

所以一般都會寫'a, 'b, 'c等,因為這樣寫起來比較快,也是常用的寫法。但如果你想的話,你可以隨時更改。有一個很好的建議是,如果代碼非常複雜,把生命期改成一個 "人類可讀"的名字可以幫助你閱讀代碼。

我們再來看看與trait的比較,對於泛型。比如說

use std::fmt::Display;

fn prints<T: Display>(input: T) {
    println!("T is {}", input);
}

fn main() {}

當你寫T: Display的時候,它的意思是 "只有當T有Display時,才取T"。 而不是說: "我把Display給T".

對於生命期也是如此。當你在這裡寫 'a:

#[derive(Debug)]
struct City<'a> {
    name: &'a str,
    date_founded: u32,
}

fn main() {}

意思是 "如果name的生命期至少與City一樣長,才接受name的輸入"。 它的意思不是說: "我會讓name的輸入與City一樣長壽"。

現在我們可以瞭解一下之前看到的<'_>。這被稱為 "匿名生命期",是使用引用的一個指標。例如,當你在實現結構時,Rust會向你建議使用。這裡有一個幾乎可以工作的結構體,但還不能工作:

    // ⚠️
struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

fn main() {}

所以我們對struct做了我們需要做的事情:首先我們說name來自於一個&str。這就意味著我們需要lifetime,所以我們給了它<'a>。然後我們必須對struct做同樣的處理,以證明它們至少和這個生命期一樣長。但是Rust卻告訴我們要這樣做:

error[E0726]: implicit elided lifetime not allowed here
 --> src\main.rs:6:6
  |
6 | impl Adventurer {
  |      ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`

它想讓我們加上那個匿名的生命期,以表明有一個引用被使用。所以如果我們這樣寫,它就會很高興。

struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer<'_> {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

fn main() {}

這個生命期是為了讓你不必總是寫諸如impl<'a> Adventurer<'a>這樣的東西,因為結構已經顯示了生命期。

在Rust中,生命期是很困難的,但這裡有一些技巧可以避免對它們太過緊張。

  • 你可以繼續使用自有類型,使用克隆等,如果你想暫時避免它們。
  • 很多時候,當編譯器想要lifetime的時候,你只要在這裡和那裡寫上<'a>就可以了。這只是一種 "別擔心,我不會給你任何不夠長壽的東西"的說法。
  • 你可以每次只探索一下生命期。寫一些擁有值的代碼,然後把一個代碼變成一個引用。編譯器會開始抱怨,但也會給出一些建議。如果它變得太複雜,你可以撤銷它,下次再試。

讓我們用我們的代碼來做這個,看看編譯器怎麼說。首先我們回去把生命期拿出來,同時實現DisplayDisplay就打印Adventurer的名字。

// ⚠️
struct Adventurer {
    name: &str,
    hit_points: u32,
}

impl Adventurer {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

impl std::fmt::Display for Adventurer {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{} has {} hit points.", self.name, self.hit_points)
        }
}

fn main() {}

第一個抱怨就是這個:

error[E0106]: missing lifetime specifier
 --> src\main.rs:2:11
  |
2 |     name: &str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 | struct Adventurer<'a> {
2 |     name: &'a str,
  |

它建議怎麼做:在Adventurer後面加上<'a>,以及&'a str。所以我們就這麼做。

// ⚠️
struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

impl std::fmt::Display for Adventurer {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{} has {} hit points.", self.name, self.hit_points)
        }
}

fn main() {}

現在它對這些部分很滿意,但對impl塊感到奇怪。它希望我們提到它在使用引用。

error[E0726]: implicit elided lifetime not allowed here
 --> src\main.rs:6:6
  |
6 | impl Adventurer {
  |      ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`

error[E0726]: implicit elided lifetime not allowed here
  --> src\main.rs:12:28
   |
12 | impl std::fmt::Display for Adventurer {
   |                            ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`

好了,我們將這些寫進去......現在它工作了!現在我們可以創建一個Adventurer,然後用它做一些事情:

struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer<'_> {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

impl std::fmt::Display for Adventurer<'_> {

        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{} has {} hit points.", self.name, self.hit_points)
        }
}

fn main() {
    let mut billy = Adventurer {
        name: "Billy",
        hit_points: 100_000,
    };
    println!("{}", billy);
    billy.take_damage();
}

這個將打印:

Billy has 100000 hit points.
Billy has 99980 hit points left!

所以你可以看到,lifetimes往往只是編譯器想要確定。而且它通常很聰明,幾乎可以猜到你想要的生命期,只需要你告訴它,它就可以確定了。

內部可變性

Cell

內部可變性的意思是在內部有一點可變性。還記得在Rust中,你需要用mut來改變一個變量嗎?也有一些方法可以不用mut這個詞來改變它們。這是因為Rust有一些方法可以讓你安全地在一個不可變的結構裡面改變值。每一種方法都遵循一些規則,確保改變值仍然是安全的。

首先,讓我們看一個簡單的例子,我們會想要這樣做:想象一下,一個叫PhoneModel的結構體有很多字段:

struct PhoneModel {
    company_name: String,
    model_name: String,
    screen_size: f32,
    memory: usize,
    date_issued: u32,
    on_sale: bool,
}

fn main() {
    let super_phone_3000 = PhoneModel {
        company_name: "YY Electronics".to_string(),
        model_name: "Super Phone 3000".to_string(),
        screen_size: 7.5,
        memory: 4_000_000,
        date_issued: 2020,
        on_sale: true,
    };

}

PhoneModel中的字段最好是不可變的,因為我們不希望數據改變。比如說date_issuedscreen_size永遠不會變。

但是裡面有一個字段叫on_sale。一個手機型號先是會有銷售(true),但是後來公司會停止銷售。我們能不能只讓這一個字段可變?因為我們不想寫let mut super_phone_3000。如果我們這樣做,那麼每個字段都會變得可變。

Rust有很多方法可以讓一些不可變的東西里面有一些安全的可變性,最簡單的方法叫做Cell。首先我們使用use std::cell::Cell,這樣我們就可以每次只寫Cell而不是std::cell::Cell

然後我們把on_sale: bool改成on_sale: Cell<bool>。現在它不是一個bool:它是一個Cell,容納了一個bool

Cell有一個叫做.set()的方法,在這裡你可以改變值。我們用.set()on_sale: true改為on_sale: Cell::new(true)

use std::cell::Cell;

struct PhoneModel {
    company_name: String,
    model_name: String,
    screen_size: f32,
    memory: usize,
    date_issued: u32,
    on_sale: Cell<bool>,
}

fn main() {
    let super_phone_3000 = PhoneModel {
        company_name: "YY Electronics".to_string(),
        model_name: "Super Phone 3000".to_string(),
        screen_size: 7.5,
        memory: 4_000_000,
        date_issued: 2020,
        on_sale: Cell::new(true),
    };

    // 10 years later, super_phone_3000 is not on sale anymore
    super_phone_3000.on_sale.set(false);
}

Cell 適用於所有類型,但對簡單的 Copy 類型效果最好,因為它給出的是值,而不是引用。Cell有一個叫做get()的方法,它只對Copy類型有效。

另一個可以使用的類型是 RefCell

RefCell

RefCell是另一種無需聲明mut而改變值的方法。它的意思是 "引用單元格",就像 Cell,但使用引用而不是副本。

我們將創建一個 User 結構。到目前為止,你可以看到它與 Cell 類似。

use std::cell::RefCell;

#[derive(Debug)]
struct User {
    id: u32,
    year_registered: u32,
    username: String,
    active: RefCell<bool>,
    // Many other fields
}

fn main() {
    let user_1 = User {
        id: 1,
        year_registered: 2020,
        username: "User 1".to_string(),
        active: RefCell::new(true),
    };

    println!("{:?}", user_1.active);
}

這樣就可以打印出RefCell { value: true }

RefCell的方法有很多。其中兩種是.borrow().borrow_mut()。使用這些方法,你可以做與&&mut相同的事情。規則都是一樣的:

  • 多個不可變借用可以
  • 一個可變的借用可以
  • 但可變和不可變借用在一起是不行的

所以改變RefCell中的值是非常容易的。

#![allow(unused)]
fn main() {
// 🚧
user_1.active.replace(false);
println!("{:?}", user_1.active);
}

而且還有很多其他的方法,比如replace_with使用的是閉包。

#![allow(unused)]
fn main() {
// 🚧
let date = 2020;

user_1
    .active
    .replace_with(|_| if date < 2000 { true } else { false });
println!("{:?}", user_1.active);
}

但是你要小心使用RefCell,因為它是在運行時而不是編譯時檢查借用。運行時是指程序實際運行的時候(編譯後)。所以這將會被編譯,即使它是錯誤的。

use std::cell::RefCell;

#[derive(Debug)]
struct User {
    id: u32,
    year_registered: u32,
    username: String,
    active: RefCell<bool>,
    // Many other fields
}

fn main() {
    let user_1 = User {
        id: 1,
        year_registered: 2020,
        username: "User 1".to_string(),
        active: RefCell::new(true),
    };

    let borrow_one = user_1.active.borrow_mut(); // first mutable borrow - okay
    let borrow_two = user_1.active.borrow_mut(); // second mutable borrow - not okay
}

但如果你運行它,它就會立即崩潰。

thread 'main' panicked at 'already borrowed: BorrowMutError', C:\Users\mithr\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib/rustlib/src/rust\src\libcore\cell.rs:877:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\rust_book.exe` (exit code: 101)

already borrowed: BorrowMutError是重要的部分。所以當你使用RefCell時,好編譯運行檢查。

Mutex

Mutex是另一種改變數值的方法,不需要聲明mut。Mutex的意思是mutual exclusion,也就是 "一次只能改一個"。這就是為什麼Mutex是安全的,因為它每次只讓一個進程改變它。為了做到這一點,它使用了.lock()Lock就像從裡面鎖上一扇門。你進入一個房間,鎖上門,現在你可以在房間裡面改變東西。別人不能進來阻止你,因為你把門鎖上了。

Mutex通過例子更容易理解:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5); // A new Mutex<i32>. We don't need to say mut
    let mut mutex_changer = my_mutex.lock().unwrap(); // mutex_changer is a MutexGuard
                                                     // It has to be mut because we will change it
                                                     // Now it has access to the Mutex
                                                     // Let's print my_mutex to see:

    println!("{:?}", my_mutex); // This prints "Mutex { data: <locked> }"
                                // So we can't access the data with my_mutex now,
                                // only with mutex_changer

    println!("{:?}", mutex_changer); // This prints 5. Let's change it to 6.

    *mutex_changer = 6; // mutex_changer is a MutexGuard<i32> so we use * to change the i32

    println!("{:?}", mutex_changer); // Now it says 6
}

但是mutex_changer做完後還是有鎖。我們該如何阻止它呢?MutexMutexGuard超出範圍時就會被解鎖。"超出範圍"表示該代碼塊已經完成。比如說:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);
    {
        let mut mutex_changer = my_mutex.lock().unwrap();
        *mutex_changer = 6;
    } // mutex_changer goes out of scope - now it is gone. It is not locked anymore

    println!("{:?}", my_mutex); // Now it says: Mutex { data: 6 }
}

如果你不想使用不同的{}代碼塊,你可以使用std::mem::drop(mutex_changer)std::mem::drop的意思是 "讓這個超出範圍"。

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);
    let mut mutex_changer = my_mutex.lock().unwrap();
    *mutex_changer = 6;
    std::mem::drop(mutex_changer); // drop mutex_changer - it is gone now
                                   // and my_mutex is unlocked

    println!("{:?}", my_mutex); // Now it says: Mutex { data: 6 }
}

你必須小心使用 Mutex,因為如果另一個變量試圖 lock它,它會等待。

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);
    let mut mutex_changer = my_mutex.lock().unwrap(); // mutex_changer has the lock
    let mut other_mutex_changer = my_mutex.lock().unwrap(); // other_mutex_changer wants the lock
                                                            // the program is waiting
                                                            // and waiting
                                                            // and will wait forever.

    println!("This will never print...");
}

還有一種方法是try_lock()。然後它會試一次,如果沒能鎖上就會放棄。try_lock().unwrap()就不要做了,因為如果不成功它就會崩潰。if letmatch比較好。

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);
    let mut mutex_changer = my_mutex.lock().unwrap();
    let mut other_mutex_changer = my_mutex.try_lock(); // try to get the lock

    if let Ok(value) = other_mutex_changer {
        println!("The MutexGuard has: {}", value)
    } else {
        println!("Didn't get the lock")
    }
}

另外,你不需要創建一個變量來改變Mutex。你可以直接這樣做:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);

    *my_mutex.lock().unwrap() = 6;

    println!("{:?}", my_mutex);
}

*my_mutex.lock().unwrap() = 6;的意思是 "解鎖my_mutex並使其成為6"。沒有任何變量來保存它,所以你不需要調用 std::mem::drop。如果你願意,你可以做100次--這並不重要。

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);

    for _ in 0..100 {
        *my_mutex.lock().unwrap() += 1; // locks and unlocks 100 times
    }

    println!("{:?}", my_mutex);
}

RwLock

RwLock的意思是 "讀寫鎖"。它像Mutex,但也像RefCell。你用.write().unwrap()代替.lock().unwrap()來改變它。但你也可以用.read().unwrap()來獲得讀權限。它和RefCell一樣,遵循這些規則:

  • 很多.read()變量可以
  • 一個.write()變量可以
  • 但多個.write().read().write()一起是不行的

如果在無法訪問的情況下嘗試.write(),程序將永遠運行。

use std::sync::RwLock;

fn main() {
    let my_rwlock = RwLock::new(5);

    let read1 = my_rwlock.read().unwrap(); // one .read() is fine
    let read2 = my_rwlock.read().unwrap(); // two .read()s is also fine

    println!("{:?}, {:?}", read1, read2);

    let write1 = my_rwlock.write().unwrap(); // uh oh, now the program will wait forever
}

所以我們用std::mem::drop,就像用Mutex一樣。

use std::sync::RwLock;
use std::mem::drop; // We will use drop() many times

fn main() {
    let my_rwlock = RwLock::new(5);

    let read1 = my_rwlock.read().unwrap();
    let read2 = my_rwlock.read().unwrap();

    println!("{:?}, {:?}", read1, read2);

    drop(read1);
    drop(read2); // we dropped both, so we can use .write() now

    let mut write1 = my_rwlock.write().unwrap();
    *write1 = 6;
    drop(write1);
    println!("{:?}", my_rwlock);
}

而且你也可以使用try_read()try_write()

use std::sync::RwLock;

fn main() {
    let my_rwlock = RwLock::new(5);

    let read1 = my_rwlock.read().unwrap();
    let read2 = my_rwlock.read().unwrap();

    if let Ok(mut number) = my_rwlock.try_write() {
        *number += 10;
        println!("Now the number is {}", number);
    } else {
        println!("Couldn't get write access, sorry!")
    };
}

Cow

Cow是一個非常方便的枚舉。它的意思是 "寫時克隆",如果你不需要String,可以返回一個&str,如果你需要,可以返回一個String。(它也可以對數組與Vec等做同樣的處理)。

為了理解它,我們看一下簽名。它說

pub enum Cow<'a, B>
where
    B: 'a + ToOwned + ?Sized,
 {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

fn main() {}

你馬上就知道,'a意味著它可以和引用一起工作。ToOwned的特性意味著它是一個可以變成擁有類型的類型。例如,str通常是一個引用(&str),你可以把它變成一個擁有的String

接下來是?Sized。這意味著 "也許是Sized,但也許不是"。Rust中幾乎每個類型都是Sized的,但像str這樣的類型卻不是。這就是為什麼我們需要一個 & 來代替 str,因為編譯器不知道大小。所以,如果你想要一個可以使用 str 這樣的trait,你可以添加 ?Sized.

接下來是enum的變種。它們是 BorrowedOwned

想象一下,你有一個返回 Cow<'static, str> 的函數。如果你告訴函數返回"My message".into(),它就會查看類型:"My message"是str. 這是一個Borrowed的類型,所以它選擇Borrowed(&'a B)。所以它就變成了Cow::Borrowed(&'static str)

而如果你給它一個format!("{}", "My message").into(),那麼它就會查看類型。這次是一個String,因為format!創建了String。所以這次會選擇 "Owned"。

下面是一個測試Cow的例子。我們將把一個數字放入一個函數中,返回一個Cow<'static, str>。根據這個數字,它會創建一個&strString。然後它使用.into()將其變成Cow。這樣做的時候,它就會選擇Cow::Borrowed或者Cow::Owned。那我們就匹配一下,看看它選的是哪一個。

use std::borrow::Cow;

fn modulo_3(input: u8) -> Cow<'static, str> {
    match input % 3 {
        0 => "Remainder is 0".into(),
        1 => "Remainder is 1".into(),
        remainder => format!("Remainder is {}", remainder).into(),
    }
}

fn main() {
    for number in 1..=6 {
        match modulo_3(number) {
            Cow::Borrowed(message) => println!("{} went in. The Cow is borrowed with this message: {}", number, message),
            Cow::Owned(message) => println!("{} went in. The Cow is owned with this message: {}", number, message),
        }
    }
}

這個打印:

1 went in. The Cow is borrowed with this message: Remainder is 1
2 went in. The Cow is owned with this message: Remainder is 2
3 went in. The Cow is borrowed with this message: Remainder is 0
4 went in. The Cow is borrowed with this message: Remainder is 1
5 went in. The Cow is owned with this message: Remainder is 2
6 went in. The Cow is borrowed with this message: Remainder is 0

Cow還有一些其他的方法,比如into_owned 或者 into_borrowed,這樣如果你需要的話,你可以改變它。

類型別名

類型別名的意思是 "給某個類型一個新的名字"。類型別名非常簡單。通常,當您有一個很長的類型,而又不想每次都寫它時,您就會使用它們。當您想給一個類型起一個更好的名字,便於記憶時,也可以使用它。下面是兩個類型別名的例子。

這裡是一個不難的類型,但是你想讓你的代碼更容易被其他人(或者你)理解。

type CharacterVec = Vec<char>;

fn main() {}

這是一種非常難讀的類型:

// this return type is extremely long
fn returns<'a>(input: &'a Vec<char>) -> std::iter::Take<std::iter::Skip<std::slice::Iter<'a, char>>> {
    input.iter().skip(4).take(5)
}

fn main() {}

所以你可以改成這樣。

type SkipFourTakeFive<'a> = std::iter::Take<std::iter::Skip<std::slice::Iter<'a, char>>>;

fn returns<'a>(input: &'a Vec<char>) -> SkipFourTakeFive {
    input.iter().skip(4).take(5)
}

fn main() {}

當然,你也可以導入元素,讓類型更短:

use std::iter::{Take, Skip};
use std::slice::Iter;

fn returns<'a>(input: &'a Vec<char>) -> Take<Skip<Iter<'a, char>>> {
    input.iter().skip(4).take(5)
}

fn main() {}

所以你可以根據自己的喜好來決定在你的代碼中什麼是最好看的。

請注意,這並沒有創建一個實際的新類型。它只是一個代替現有類型的名稱。所以如果你寫了 type File = String;,編譯器只會看到 String。所以這將打印出 true

type File = String;

fn main() {
    let my_file = File::from("I am file contents");
    let my_string = String::from("I am file contents");
    println!("{}", my_file == my_string);
}

那麼如果你想要一個實際的新類型呢?

如果你想要一個新的文件類型,而編譯器看到的是File,你可以把它放在一個結構中。

struct File(String); // File is a wrapper around String

fn main() {
    let my_file = File(String::from("I am file contents"));
    let my_string = String::from("I am file contents");
}

現在這樣就不行了,因為它們是兩種不同的類型。

struct File(String); // File is a wrapper around String

fn main() {
    let my_file = File(String::from("I am file contents"));
    let my_string = String::from("I am file contents");
    println!("{}", my_file == my_string);  // ⚠️ cannot compare File with String
}

如果你想比較裡面的String,可以用my_file.0:

struct File(String);

fn main() {
    let my_file = File(String::from("I am file contents"));
    let my_string = String::from("I am file contents");
    println!("{}", my_file.0 == my_string); // my_file.0 is a String, so this prints true
}

在函數中導入和重命名

通常你會在程序的頂部寫上use,像這樣。

use std::cell::{Cell, RefCell};

fn main() {}

但我們看到,你可以在任何地方這樣做,特別是在函數中使用名稱較長的enum。下面是一個例子:

enum MapDirection {
    North,
    NorthEast,
    East,
    SouthEast,
    South,
    SouthWest,
    West,
    NorthWest,
}

fn main() {}

fn give_direction(direction: &MapDirection) {
    match direction {
        MapDirection::North => println!("You are heading north."),
        MapDirection::NorthEast => println!("You are heading northeast."),
        // So much more left to type...
        // ⚠️ because we didn't write every possible variant
    }
}

所以現在我們要在函數裡面導入MapDirection。也就是說,在函數裡面你可以直接寫North等。

enum MapDirection {
    North,
    NorthEast,
    East,
    SouthEast,
    South,
    SouthWest,
    West,
    NorthWest,
}

fn main() {}

fn give_direction(direction: &MapDirection) {
    use MapDirection::*; // Import everything in MapDirection
    let m = "You are heading";

    match direction {
        North => println!("{} north.", m),
        NorthEast => println!("{} northeast.", m),
        // This is a bit better
        // ⚠️
    }
}

我們已經看到::*的意思是 "導入::之後的所有內容"。在我們的例子中,這意味著NorthNorthEast......一直到NorthWest。當你導入別人的代碼時,你也可以這樣做,但如果代碼非常大,你可能會有問題。如果它有一些元素和你的代碼是一樣的呢?所以一般情況下最好不要一直使用::*,除非你有把握。很多時候你在別人的代碼裡看到一個叫prelude的部分,裡面有你可能需要的所有主要元素。那麼你通常會這樣使用:name::prelude::*。 我們將在 modulescrates 的章節中更多地討論這個問題。

您也可以使用 as 來更改名稱。例如,也許你正在使用別人的代碼,而你不能改變枚舉中的名稱。

enum FileState {
    CannotAccessFile,
    FileOpenedAndReady,
    NoSuchFileExists,
    SimilarFileNameInNextDirectory,
}

fn main() {}

那麼你就可以

  1. 導入所有的東西
  2. 更改名稱
enum FileState {
    CannotAccessFile,
    FileOpenedAndReady,
    NoSuchFileExists,
    SimilarFileNameInNextDirectory,
}

fn give_filestate(input: &FileState) {
    use FileState::{
        CannotAccessFile as NoAccess,
        FileOpenedAndReady as Good,
        NoSuchFileExists as NoFile,
        SimilarFileNameInNextDirectory as OtherDirectory
    };
    match input {
        NoAccess => println!("Can't access file."),
        Good => println!("Here is your file"),
        NoFile => println!("Sorry, there is no file by that name."),
        OtherDirectory => println!("Please check the other directory."),
    }
}

fn main() {}

所以現在你可以寫OtherDirectory而不是FileState::SimilarFileNameInNextDirectory

todo!宏

有時你想粗略寫點寫代碼幫助你想象你的項目。例如,想象一個簡單的項目,用書籍做一些事情。下面是你寫的時候的想法:

struct Book {} // Okay, first I need a book struct.
               // Nothing in there yet - will add later

enum BookType { // A book can be hardcover or softcover, so add an enum
    HardCover,
    SoftCover,
}

fn get_book(book: &Book) -> Option<String> {} // ⚠️ get_book should take a &Book and return an Option<String>

fn delete_book(book: Book) -> Result<(), String> {} // delete_book should take a Book and return a Result...
                                                    // TODO: impl block and make these functions methods...
fn check_book_type(book_type: &BookType) { // Let's make sure the match statement works
    match book_type {
        BookType::HardCover => println!("It's hardcover"),
        BookType::SoftCover => println!("It's softcover"),
    }
}

fn main() {
    let book_type = BookType::HardCover;
    check_book_type(&book_type); // Okay, let's check this function!
}

但Rust對get_bookdelete_book不滿意。它說

error[E0308]: mismatched types
  --> src\main.rs:32:29
   |
32 | fn get_book(book: &Book) -> Option<String> {}
   |    --------                 ^^^^^^^^^^^^^^ expected enum `std::option::Option`, found `()`
   |    |
   |    implicitly returns `()` as its body has no tail or `return` expression
   |
   = note:   expected enum `std::option::Option<std::string::String>`
           found unit type `()`

error[E0308]: mismatched types
  --> src\main.rs:34:31
   |
34 | fn delete_book(book: Book) -> Result<(), String> {}
   |    -----------                ^^^^^^^^^^^^^^^^^^ expected enum `std::result::Result`, found `()`
   |    |
   |    implicitly returns `()` as its body has no tail or `return` expression
   |
   = note:   expected enum `std::result::Result<(), std::string::String>`
           found unit type `()`

但是你現在不關心get_bookdelete_book。這時你可以使用todo!()。如果你把這個加到函數中,Rust不會抱怨,而且會編譯。

struct Book {}

fn get_book(book: &Book) -> Option<String> {
    todo!() // todo means "I will do it later, please be quiet"
}

fn delete_book(book: Book) -> Result<(), String> {
    todo!()
}

fn main() {}

所以現在代碼編譯,你可以看到check_book_type的結果:It's hardcover

但是要小心,因為它只是編譯--你不能使用函數。如果你調用裡面有todo!()的函數,它就會崩潰。

另外,todo!()函數仍然需要真實的輸入和輸出類型。如果你只寫這個,它將無法編譯。

struct Book {}

fn get_book(book: &Book) -> WorldsBestType { // ⚠️
    todo!()
}

fn main() {}

它會說

error[E0412]: cannot find type `WorldsBestType` in this scope
  --> src\main.rs:32:29
   |
32 | fn get_book(book: &Book) -> WorldsBestType {
   |                             ^^^^^^^^^^^^^^ not found in this scope

todo!()其實和另一個宏一樣:unimplemented!()。程序員們經常使用 unimplemented!(),但打字時太長了,所以他們創建了 todo!(),它比較短。

Rc

Rc的意思是 "reference counter"(引用計數器)。你知道在Rust中,每個變量只能有一個所有者。這就是為什麼這個不能工作的原因:

fn takes_a_string(input: String) {
    println!("It is: {}", input)
}

fn also_takes_a_string(input: String) {
    println!("It is: {}", input)
}

fn main() {
    let user_name = String::from("User MacUserson");

    takes_a_string(user_name);
    also_takes_a_string(user_name); // ⚠️
}

takes_a_string取了user_name之後,你就不能再使用了。這裡沒有問題:你可以直接給它user_name.clone()。但有時一個變量是一個結構的一部分,也許你不能克隆這個結構;或者String真的很長,你不想克隆它。這些都是Rc的一些原因,它讓你擁有多個所有者。Rc就像一個優秀的辦公人員。Rc寫下誰擁有所有權,以及有多少個。然後一旦所有者的數量下降到0,這個變量就可以消失了。

下面是如何使用Rc。首先想象兩個結構:一個叫 City,另一個叫 CityDataCity有一個城市的信息,而CityData把所有的城市都放在Vec中。

#[derive(Debug)]
struct City {
    name: String,
    population: u32,
    city_history: String,
}

#[derive(Debug)]
struct CityData {
    names: Vec<String>,
    histories: Vec<String>,
}

fn main() {
    let calgary = City {
        name: "Calgary".to_string(),
        population: 1_200_000,
           // Pretend that this string is very very long
        city_history: "Calgary began as a fort called Fort Calgary that...".to_string(),
    };

    let canada_cities = CityData {
        names: vec![calgary.name], // This is using calgary.name, which is short
        histories: vec![calgary.city_history], // But this String is very long
    };

    println!("Calgary's history is: {}", calgary.city_history);  // ⚠️
}

當然,這是不可能的,因為canada_cities現在擁有數據,而calgary沒有。它說:

error[E0382]: borrow of moved value: `calgary.city_history`
  --> src\main.rs:27:42
   |
24 |         histories: vec![calgary.city_history], // But this String is very long
   |                         -------------------- value moved here
...
27 |     println!("Calgary's history is: {}", calgary.city_history);  // ⚠️
   |                                          ^^^^^^^^^^^^^^^^^^^^ value borrowed here after move
   |
   = note: move occurs because `calgary.city_history` has type `std::string::String`, which does not implement the `Copy` trait

我們可以克隆名稱:names: vec![calgary.name.clone()],但是我們不想克隆city_history,因為它很長。所以我們可以用一個Rc

增加use的聲明。

use std::rc::Rc;

fn main() {}

然後用RcString包圍起來:

use std::rc::Rc;

#[derive(Debug)]
struct City {
    name: String,
    population: u32,
    city_history: Rc<String>,
}

#[derive(Debug)]
struct CityData {
    names: Vec<String>,
    histories: Vec<Rc<String>>,
}

fn main() {}

要添加一個新的引用,你必須clone Rc。但是等一下,我們不是想避免使用.clone()嗎?不完全是:我們不想克隆整個String。但是一個Rc的克隆只是克隆了指針--它基本上是沒有開銷的。這就像在一盒書上貼上一個名字貼紙,以表明有兩個人擁有它,而不是做一盒全新的書。

你可以用item.clone()或者用Rc::clone(&item)來克隆一個叫itemRc。所以calgary.city_history有兩個所有者。 我們可以用Rc::strong_count(&item)查詢擁有者數量。另外我們再增加一個新的所有者。現在我們的代碼是這樣的:

use std::rc::Rc;

#[derive(Debug)]
struct City {
    name: String,
    population: u32,
    city_history: Rc<String>, // String inside an Rc
}

#[derive(Debug)]
struct CityData {
    names: Vec<String>,
    histories: Vec<Rc<String>>, // A Vec of Strings inside Rcs
}

fn main() {
    let calgary = City {
        name: "Calgary".to_string(),
        population: 1_200_000,
           // Pretend that this string is very very long
        city_history: Rc::new("Calgary began as a fort called Fort Calgary that...".to_string()), // Rc::new() to make the Rc
    };

    let canada_cities = CityData {
        names: vec![calgary.name],
        histories: vec![calgary.city_history.clone()], // .clone() to increase the count
    };

    println!("Calgary's history is: {}", calgary.city_history);
    println!("{}", Rc::strong_count(&calgary.city_history));
    let new_owner = calgary.city_history.clone();
}

這就打印出了2。而new_owner現在是Rc<String>。現在如果我們用println!("{}", Rc::strong_count(&calgary.city_history));,我們得到3

那麼,如果有強指針,是否有弱指針呢?是的,有。弱指針是有用的,因為如果兩個Rc互相指向對方,它們就不會死。這就是所謂的 "引用循環"。如果第1項對第2項有一個Rc,而第2項對第1項有一個Rc,它們不能到0,在這種情況下,要使用弱引用。那麼Rc就會對引用進行計數,但如果只有弱引用,那麼它就會死掉。你使用Rc::downgrade(&item)而不是Rc::clone(&item)來創建弱引用。另外,需要用Rc::weak_count(&item)來查看弱引用數。

多線程

如果你使用多個線程,你可以同時做很多事情。現代計算機有一個以上的核心,所以它們可以同時做多件事情,Rust讓你使用它們。Rust使用的線程被稱為 "OS線程"。OS線程意味著操作系統在不同的核上創建線程。(其他一些語言使用 "green threads",功能較少)

你用std::thread::spawn創建線程,然後用一個閉包來告訴它該怎麼做。線程很有趣,因為它們同時運行,你可以測試它,看看會發生什麼。下面是一個簡單的例子。

fn main() {
    std::thread::spawn(|| {
        println!("I am printing something");
    });
}

如果你運行這個,每次都會不一樣。有時會打印,有時不會打印(這也取決於你的電腦速度)。這是因為有時main()在線程完成之前就完成了。而當main()完成後,程序就結束了。這在for循環中更容易看到。

fn main() {
    for _ in 0..10 { // set up ten threads
        std::thread::spawn(|| {
            println!("I am printing something");
        });
    }   // Now the threads start.
}       // How many can finish before main() ends here?

通常在main結束之前,大約會打印出四條線程,但總是不一樣。如果你的電腦速度比較快,那麼可能就不會打印了。另外,有時線程會崩潰。

thread 'thread 'I am printing something
thread '<unnamed><unnamed>thread '' panicked at '<unnamed>I am printing something
' panicked at 'thread '<unnamed>cannot access stdout during shutdown' panicked at '<unnamed>thread 'cannot access stdout during
shutdown

這是在程序關閉時,線程試圖做一些正確的事情時出現的錯誤。

你可以給電腦做一些事情,這樣它就不會馬上關閉了。

fn main() {
    for _ in 0..10 {
        std::thread::spawn(|| {
            println!("I am printing something");
        });
    }
    for _ in 0..1_000_000 { // make the program declare "let x = 9" one million times
                            // It has to finish this before it can exit the main function
        let _x = 9;
    }
}

但這是一個讓線程有時間完成的愚蠢方法。更好的方法是將線程綁定到一個變量上。如果你加上 let,你就能創建一個 JoinHandle。你可以在spawn的簽名中看到這一點:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,

(f是閉包--我們將在後面學習如何將閉包放入我們的函數中)

所以現在我們每次都有JoinHandle

fn main() {
    for _ in 0..10 {
        let handle = std::thread::spawn(|| {
            println!("I am printing something");
        });

    }
}

handle現在是JoinHandle。我們怎麼處理它呢?我們使用一個叫做 .join() 的方法。這個方法的意思是 "等待所有線程完成"(它等待線程加入它)。所以現在只要寫handle.join(),它就會等待每個線程完成。

fn main() {
    for _ in 0..10 {
        let handle = std::thread::spawn(|| {
            println!("I am printing something");
        });

        handle.join(); // Wait for the threads to finish
    }
}

現在我們就來瞭解一下三種類型的閉包。這三種類型是

  • FnOnce: 取整個值
  • FnMut: 取一個可變引用
  • Fn: 取一個普通引用

如果可以的話,閉包會盡量使用Fn。但如果它需要改變值,它將使用 FnMut,而如果它需要取整個值,它將使用 FnOnceFnOnce是個好名字,因為它解釋了它的作用:它取一次值,然後就不能再取了。

下面是一個例子。

fn main() {
    let my_string = String::from("I will go into the closure");
    let my_closure = || println!("{}", my_string);
    my_closure();
    my_closure();
}

String沒有實現Copy,所以my_closure()Fn: 它拿到一個引用

如果我們改變my_string,它變成FnMut

fn main() {
    let mut my_string = String::from("I will go into the closure");
    let mut my_closure = || {
        my_string.push_str(" now");
        println!("{}", my_string);
    };
    my_closure();
    my_closure();
}

這個打印:

I will go into the closure now
I will go into the closure now now

而如果按值獲取,則是FnOnce

fn main() {
    let my_vec: Vec<i32> = vec![8, 9, 10];
    let my_closure = || {
        my_vec
            .into_iter() // into_iter takes ownership
            .map(|x| x as u8) // turn it into u8
            .map(|x| x * 2) // multiply by 2
            .collect::<Vec<u8>>() // collect into a Vec
    };
    let new_vec = my_closure();
    println!("{:?}", new_vec);
}

我們是按值取的,所以我們不能多跑my_closure()次。這就是名字的由來。

那麼現在回到線程。讓我們試著從外部引入一個值:

fn main() {
    let mut my_string = String::from("Can I go inside the thread?");

    let handle = std::thread::spawn(|| {
        println!("{}", my_string); // ⚠️
    });

    handle.join();
}

編譯器說這個不行。

error[E0373]: closure may outlive the current function, but it borrows `my_string`, which is owned by the current function
  --> src\main.rs:28:37
   |
28 |     let handle = std::thread::spawn(|| {
   |                                     ^^ may outlive borrowed value `my_string`
29 |         println!("{}", my_string);
   |                        --------- `my_string` is borrowed here
   |
note: function requires argument type to outlive `'static`
  --> src\main.rs:28:18
   |
28 |       let handle = std::thread::spawn(|| {
   |  __________________^
29 | |         println!("{}", my_string);
30 | |     });
   | |______^
help: to force the closure to take ownership of `my_string` (and any other referenced variables), use the `move` keyword
   |
28 |     let handle = std::thread::spawn(move || {
   |                                     ^^^^^^^

這條信息很長,但很有用:它說到use the `move` keyword。問題是我們可以在線程使用my_string時對它做任何事情,但線程並不擁有它。這將是不安全的。

讓我們試試其他行不通的東西。

fn main() {
    let mut my_string = String::from("Can I go inside the thread?");

    let handle = std::thread::spawn(|| {
        println!("{}", my_string); // now my_string is being used as a reference
    });

    std::mem::drop(my_string);  // ⚠️ We try to drop it here. But the thread still needs it.

    handle.join();
}

所以你要用move來取值,現在安全了:

fn main() {
    let mut my_string = String::from("Can I go inside the thread?");

    let handle = std::thread::spawn(move|| {
        println!("{}", my_string);
    });

    std::mem::drop(my_string);  // ⚠️ we can't drop, because handle has it. So this won't work

    handle.join();
}

所以我們把std::mem::drop刪掉,現在就可以了。handlemy_string,我們的代碼就安全了。

fn main() {
    let mut my_string = String::from("Can I go inside the thread?");

    let handle = std::thread::spawn(move|| {
        println!("{}", my_string);
    });

    handle.join();
}

所以只要記住:如果你在線程中需要一個來自線程外的值,你需要使用move

函數中的閉包

閉包是偉大的。那麼我們如何把它們放到自己的函數中呢?

你可以創建自己的函數來接受閉包,但是在函數裡面就不那麼自由了,你必須決定類型。在函數外部,一個閉包可以在FnFnMutFnOnce之間自行決定,但在函數內部你必須選擇一個。最好的理解方式是看幾個函數簽名。 這裡是.all()的那個。我們記得,它檢查一個迭代器,看看所有的東西是否是true(取決於你決定是true還是false)。它的部分簽名是這樣說的。

#![allow(unused)]
fn main() {
    fn all<F>(&mut self, f: F) -> bool    // 🚧
    where
        F: FnMut(Self::Item) -> bool,
}

fn all<F>:這告訴你有一個通用類型F。一個閉包總是泛型,因為每次都是不同的類型。

(&mut self, f: F):&mut self告訴你這是一個方法。f: F通常你看到的是一個閉包:這是變量名和類型。 當然,fF並沒有什麼特別之處,它們可以是不同的名字。如果你願意,你可以寫my_closure: Closure--這並不重要。但在簽名中,你幾乎總是看到f: F

接下來是關於閉包的部分:F: FnMut(Self::Item) -> bool。在這裡,它決定了閉包是 FnMut,所以它可以改變值。它改變了Self::Item的值,這是它所取的迭代器。而且它必須返回 truefalse

這裡是一個更簡單的簽名,有一個閉包。

#![allow(unused)]
fn main() {
fn do_something<F>(f: F)    // 🚧
where
    F: FnOnce(),
{
    f();
}
}

這只是說它接受一個閉包,取值(FnOnce=取值),而不返回任何東西。所以現在我們可以調用這個什麼都不取的閉包,做我們喜歡做的事情。我們將創建一個 Vec,然後對它進行迭代,只是為了展示我們現在可以做什麼。

fn do_something<F>(f: F)
where
    F: FnOnce(),
{
    f();
}

fn main() {
    let some_vec = vec![9, 8, 10];
    do_something(|| {
        some_vec
            .into_iter()
            .for_each(|x| println!("The number is: {}", x));
    })
}

一個更真實的例子,我們將再次創建一個 City 結構體。這次 City 結構體有更多關於年份和人口的數據。它有一個 Vec<u32> 來表示所有的年份,還有一個 Vec<u32> 來表示所有的人口。

City有兩個方法:new()用於創建一個新的City, .city_data()有個閉包參數。當我們使用 .city_data() 時,它給我們提供了年份和人口以及一個閉包,所以我們可以對數據做我們想做的事情。閉包類型是 FnMut,所以我們可以改變數據。它看起來像這樣:

#[derive(Debug)]
struct City {
    name: String,
    years: Vec<u32>,
    populations: Vec<u32>,
}

impl City {
    fn new(name: &str, years: Vec<u32>, populations: Vec<u32>) -> Self {

        Self {
            name: name.to_string(),
            years,
            populations,
        }
    }

    fn city_data<F>(&mut self, mut f: F) // We bring in self, but only f is generic F. f is the closure

    where
        F: FnMut(&mut Vec<u32>, &mut Vec<u32>), // The closure takes mutable vectors of u32
                                                // which are the year and population data
    {
        f(&mut self.years, &mut self.populations) // Finally this is the actual function. It says
                                                  // "use a closure on self.years and self.populations"
                                                  // We can do whatever we want with the closure
    }
}

fn main() {
    let years = vec![
        1372, 1834, 1851, 1881, 1897, 1925, 1959, 1989, 2000, 2005, 2010, 2020,
    ];
    let populations = vec![
        3_250, 15_300, 24_000, 45_900, 58_800, 119_800, 283_071, 478_974, 400_378, 401_694,
        406_703, 437_619,
    ];
    // Now we can create our city
    let mut tallinn = City::new("Tallinn", years, populations);

    // Now we have a .city_data() method that has a closure. We can do anything we want.

    // First let's put the data for 5 years together and print it.
    tallinn.city_data(|city_years, city_populations| { // We can call the input anything we want
        let new_vec = city_years
            .into_iter()
            .zip(city_populations.into_iter()) // Zip the two together
            .take(5)                           // but only take the first 5
            .collect::<Vec<(_, _)>>(); // Tell Rust to decide the type inside the tuple
        println!("{:?}", new_vec);
    });

    // Now let's add some data for the year 2030
    tallinn.city_data(|x, y| { // This time we just call the input x and y
        x.push(2030);
        y.push(500_000);
    });

    // We don't want the 1834 data anymore
    tallinn.city_data(|x, y| {
        let position_option = x.iter().position(|x| *x == 1834);
        if let Some(position) = position_option {
            println!(
                "Going to delete {} at position {:?} now.",
                x[position], position
            ); // Confirm that we delete the right item
            x.remove(position);
            y.remove(position);
        }
    });

    println!(
        "Years left are {:?}\nPopulations left are {:?}",
        tallinn.years, tallinn.populations
    );
}

這將打印出我們調用.city_data().的所有時間的結果:

[(1372, 3250), (1834, 15300), (1851, 24000), (1881, 45900), (1897, 58800)]
Going to delete 1834 at position 1 now.
Years left are [1372, 1851, 1881, 1897, 1925, 1959, 1989, 2000, 2005, 2010, 2020, 2030]
Populations left are [3250, 24000, 45900, 58800, 119800, 283071, 478974, 400378, 401694, 406703, 437619, 500000]

impl Trait

impl Trait與泛型類似。你還記得,泛型使用一個類型 T(或任何其他名稱),然後在程序編譯時決定。首先我們來看一個具體的類型:

fn gives_higher_i32(one: i32, two: i32) {
    let higher = if one > two { one } else { two };
    println!("{} is higher.", higher);
}

fn main() {
    gives_higher_i32(8, 10);
}

這個打印:10 is higher..

但是這個只接受i32,所以現在我們要把它做成通用的。我們需要比較,我們需要用{}打印,所以我們的類型T需要PartialOrdDisplay。記住,這意味著 "只接受已經實現PartialOrdDisplay的類型"。

use std::fmt::Display;

fn gives_higher_i32<T: PartialOrd + Display>(one: T, two: T) {
    let higher = if one > two { one } else { two };
    println!("{} is higher.", higher);
}

fn main() {
    gives_higher_i32(8, 10);
}

現在我們來看看impl Trait,它也是類似的。我們可以引入一個類型 impl Trait,而不是 T。然後它將帶入一個實現該特性的類型。這幾乎是一樣的。

fn prints_it(input: impl Into<String> + std::fmt::Display) { // Takes anything that can turn into a String and has Display
    println!("You can print many things, including {}", input);
}

fn main() {
    let name = "Tuon";
    let string_name = String::from("Tuon");
    prints_it(name);
    prints_it(string_name);
}

然而,更有趣的是,我們可以返回 impl Trait,這讓我們可以返回閉包,因為它們的函數簽名是trait。你可以在有它們的方法的簽名中看到這一點。例如,這是 .map() 的簽名。

#![allow(unused)]
fn main() {
fn map<B, F>(self, f: F) -> Map<Self, F>     // 🚧
    where
        Self: Sized,
        F: FnMut(Self::Item) -> B,
    {
        Map::new(self, f)
    }
}

fn map<B, F>(self, f: F)的意思是,它需要兩個通用類型。F是指從實現.map()的容器中取一個元素的函數,B是該函數的返回類型。然後在where之後,我們看到的是trait bound。("trait bound"的意思是 "它必須有這個trait"。)一個是Sized,接下來是閉包簽名。它必須是一個 FnMut,並在 Self::Item 上做閉包,也就是你給它的迭代器。然後它返回B

所以我們可以用同樣的方法來返回一個閉包。要返回一個閉包,使用 impl,然後是閉包簽名。一旦你返回它,你就可以像使用一個函數一樣使用它。下面是一個函數的小例子,它根據你輸入的文本給出一個閉包。如果你輸入 "double "或 "triple",那麼它就會把它乘以2或3,否則就會返給你相同的數字。因為它是一個閉包,我們可以做任何我們想做的事情,所以我們也打印一條信息。

fn returns_a_closure(input: &str) -> impl FnMut(i32) -> i32 {
    match input {
        "double" => |mut number| {
            number *= 2;
            println!("Doubling number. Now it is {}", number);
            number
        },
        "triple" => |mut number| {
            number *= 40;
            println!("Tripling number. Now it is {}", number);
            number
        },
        _ => |number| {
            println!("Sorry, it's the same: {}.", number);
            number
        },
    }
}

fn main() {
    let my_number = 10;

    // Make three closures
    let mut doubles = returns_a_closure("double");
    let mut triples = returns_a_closure("triple");
    let mut quadruples = returns_a_closure("quadruple");

    doubles(my_number);
    triples(my_number);
    quadruples(my_number);
}

下面是一個比較長的例子。讓我們想象一下,在一個遊戲中,你的角色面對的是晚上比較強的怪物。我們可以創建一個叫TimeOfDay的枚舉來記錄一天的情況。你的角色叫西蒙,有一個叫character_fear的數字,也就是f64。它晚上上升,白天下降。我們將創建一個change_fear函數,改變他的恐懼,但也做其他事情,如寫消息。它大概是這樣的:

enum TimeOfDay { // just a simple enum
    Dawn,
    Day,
    Sunset,
    Night,
}

fn change_fear(input: TimeOfDay) -> impl FnMut(f64) -> f64 { // The function takes a TimeOfDay. It returns a closure.
                                                             // We use impl FnMut(64) -> f64 to say that it needs to
                                                             // change the value, and also gives the same type back.
    use TimeOfDay::*; // So we only have to write Dawn, Day, Sunset, Night
                      // Instead of TimeOfDay::Dawn, TimeOfDay::Day, etc.
    match input {
        Dawn => |x| { // This is the variable character_fear that we give it later
            println!("The morning sun has vanquished the horrible night. You no longer feel afraid.");
            println!("Your fear is now {}", x * 0.5);
            x * 0.5
        },
        Day => |x| {
            println!("What a nice day. Maybe put your feet up and rest a bit.");
            println!("Your fear is now {}", x * 0.2);
            x * 0.2
        },
        Sunset => |x| {
            println!("The sun is almost down! This is no good.");
            println!("Your fear is now {}", x * 1.4);
            x * 1.4
        },
        Night => |x| {
            println!("What a horrible night to have a curse.");
            println!("Your fear is now {}", x * 5.0);
            x * 5.0
        },
    }
}

fn main() {
    use TimeOfDay::*;
    let mut character_fear = 10.0; // Start Simon with 10

    let mut daytime = change_fear(Day); // Make four closures here to call every time we want to change Simon's fear.
    let mut sunset = change_fear(Sunset);
    let mut night = change_fear(Night);
    let mut morning = change_fear(Dawn);

    character_fear = daytime(character_fear); // Call the closures on Simon's fear. They give a message and change the fear number.
                                              // In real life we would have a Character struct and use it as a method instead,
                                              // like this: character_fear.daytime()
    character_fear = sunset(character_fear);
    character_fear = night(character_fear);
    character_fear = morning(character_fear);
}

這個打印:

What a nice day. Maybe put your feet up and rest a bit.
Your fear is now 2
The sun is almost down! This is no good.
Your fear is now 2.8
What a horrible night to have a curse.
Your fear is now 14
The morning sun has vanquished the horrible night. You no longer feel afraid.
Your fear is now 7

Arc

你還記得我們用Rc來給一個變量一個以上的所有者。如果我們在線程中做同樣的事情,我們需要一個 ArcArc的意思是 "atomic reference counter"(原子引用計數器)。原子的意思是它使用計算機的處理器,所以每次只寫一次數據。這一點很重要,因為如果兩個線程同時寫入數據,你會得到錯誤的結果。例如,想象一下,如果你能在Rust中做到這一點。

#![allow(unused)]
fn main() {
// 🚧
let mut x = 10;

for i in 0..10 { // Thread 1
    x += 1
}
for i in 0..10 { // Thread 2
    x += 1
}
}

如果線程1和線程2一起啟動,也許就會出現這種情況。

  • 線程1看到10,寫下11,然後線程2看到11,寫下12 然後線程2看到11,寫入12。到目前為止沒有問題。
  • 線程1看到12。同時,線程2看到12。線程一看到13,寫下13 線程2也寫了13 現在我們有13個,但應該是14個 Now we have 13, but it should be 14. 這是個大問題。

Arc使用處理器來確保這種情況不會發生,所以當你有線程時必須使用這種方法。不過不建議單線程上用Arc,因為Rc更快一些。

不過你不能只用一個Arc來改變數據。所以你用一個Mutex把數據包起來,然後用一個ArcMutex包起來。

所以我們用一個Mutex在一個Arc裡面來改變一個數字的值。首先我們設置一個線程。

fn main() {

    let handle = std::thread::spawn(|| {
        println!("The thread is working!") // Just testing the thread
    });

    handle.join().unwrap(); // Make the thread wait here until it is done
    println!("Exiting the program");
}

到目前為止,這個只打印:

The thread is working!
Exiting the program

很好,現在讓我們把它放在for的循環中,進行0..5

fn main() {

    let handle = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("The thread is working!")
        }
    });

    handle.join().unwrap();
    println!("Exiting the program");
}

這也是可行的。我們得到以下結果:

The thread is working!
The thread is working!
The thread is working!
The thread is working!
The thread is working!
Exiting the program

現在我們再加一個線程。每個線程都會做同樣的事情。你可以看到,這些線程是在同一時間工作的。有時會先打印Thread 1 is working!,但其他時候Thread 2 is working!先打印。這就是所謂的併發,也就是 "一起運行"的意思。

fn main() {

    let thread1 = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("Thread 1 is working!")
        }
    });

    let thread2 = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("Thread 2 is working!")
        }
    });

    thread1.join().unwrap();
    thread2.join().unwrap();
    println!("Exiting the program");
}

這將打印:

Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Exiting the program

現在我們要改變my_number的數值。現在它是一個i32。我們將把它改為 Arc<Mutex<i32>>:一個可以改變的 i32,由 Arc 保護。

#![allow(unused)]
fn main() {
// 🚧
let my_number = Arc::new(Mutex::new(0));
}

現在我們有了這個,我們可以克隆它。每個克隆可以進入不同的線程。我們有兩個線程,所以我們將做兩個克隆。

#![allow(unused)]
fn main() {
// 🚧
let my_number = Arc::new(Mutex::new(0));

let my_number1 = Arc::clone(&my_number); // This clone goes into Thread 1
let my_number2 = Arc::clone(&my_number); // This clone goes into Thread 2
}

現在,我們已經將安全克隆連接到my_number,我們可以將它們move到其他線程中,沒有問題。

use std::sync::{Arc, Mutex};

fn main() {
    let my_number = Arc::new(Mutex::new(0));

    let my_number1 = Arc::clone(&my_number);
    let my_number2 = Arc::clone(&my_number);

    let thread1 = std::thread::spawn(move || { // Only the clone goes into Thread 1
        for _ in 0..10 {
            *my_number1.lock().unwrap() +=1; // Lock the Mutex, change the value
        }
    });

    let thread2 = std::thread::spawn(move || { // Only the clone goes into Thread 2
        for _ in 0..10 {
            *my_number2.lock().unwrap() += 1;
        }
    });

    thread1.join().unwrap();
    thread2.join().unwrap();
    println!("Value is: {:?}", my_number);
    println!("Exiting the program");
}

程序打印:

Value is: Mutex { data: 20 }
Exiting the program

所以這是一個成功的案例。

然後我們可以將兩個線程連接在一起,形成一個for循環,並使代碼更短。

我們需要保存句柄,這樣我們就可以在循環外對每個線程調用.join()。如果我們在循環內這樣做,它將等待第一個線程完成後再啟動新的線程。

use std::sync::{Arc, Mutex};

fn main() {
    let my_number = Arc::new(Mutex::new(0));
    let mut handle_vec = vec![]; // JoinHandles will go in here

    for _ in 0..2 { // do this twice
        let my_number_clone = Arc::clone(&my_number); // Make the clone before starting the thread
        let handle = std::thread::spawn(move || { // Put the clone in
            for _ in 0..10 {
                *my_number_clone.lock().unwrap() += 1;
            }
        });
        handle_vec.push(handle); // save the handle so we can call join on it outside of the loop
                                 // If we don't push it in the vec, it will just die here
    }

    handle_vec.into_iter().for_each(|handle| handle.join().unwrap()); // call join on all handles
    println!("{:?}", my_number);
}

最後這個打印Mutex { data: 20 }

這看起來很複雜,但Arc<Mutex<SomeType>>>在Rust中使用的頻率很高,所以它變得很自然。另外,你也可以隨時寫你的代碼,讓它更乾淨。這裡是同樣的代碼,多了一條use語句和兩個函數。這些函數並沒有做任何新的事情,但是它們把一些代碼從main()中移出。如果你很難讀懂的話,可以嘗試重寫這樣的代碼。

use std::sync::{Arc, Mutex};
use std::thread::spawn; // Now we just write spawn

fn make_arc(number: i32) -> Arc<Mutex<i32>> { // Just a function to make a Mutex in an Arc
    Arc::new(Mutex::new(number))
}

fn new_clone(input: &Arc<Mutex<i32>>) -> Arc<Mutex<i32>> { // Just a function so we can write new_clone
    Arc::clone(&input)
}

// Now main() is easier to read
fn main() {
    let mut handle_vec = vec![]; // each handle will go in here
    let my_number = make_arc(0);

    for _ in 0..2 {
        let my_number_clone = new_clone(&my_number);
        let handle = spawn(move || {
            for _ in 0..10 {
                let mut value_inside = my_number_clone.lock().unwrap();
                *value_inside += 1;
            }
        });
        handle_vec.push(handle);    // the handle is done, so put it in the vector
    }

    handle_vec.into_iter().for_each(|handle| handle.join().unwrap()); // Make each one wait

    println!("{:?}", my_number);
}

Channels

A channel is an easy way to use many threads that send to one place.它們相當流行,因為它們很容易組合在一起。你可以在Rust中用std::sync::mpsc創建一個channel。mpsc的意思是 "多個生產者,單個消費者",所以 "many threads sending to one place"。要啟動一個通道,你可以使用 channel()。這將創建一個 Sender 和一個 Receiver,它們被綁在一起。你可以在函數簽名中看到這一點。

#![allow(unused)]
fn main() {
// 🚧
pub fn channel<T>() -> (Sender<T>, Receiver<T>)
}

所以你要選擇一個發送者的名字和一個接收者的名字。通常你會看到像let (sender, receiver) = channel();這樣的開頭。因為它是泛型函數,如果你只寫這個,Rust不會知道類型。

use std::sync::mpsc::channel;

fn main() {
    let (sender, receiver) = channel(); // ⚠️
}

編譯器說:

error[E0282]: type annotations needed for `(std::sync::mpsc::Sender<T>, std::sync::mpsc::Receiver<T>)`
  --> src\main.rs:30:30
   |
30 |     let (sender, receiver) = channel();
   |         ------------------   ^^^^^^^ cannot infer type for type parameter `T` declared on the function `channel`
   |         |
   |         consider giving this pattern the explicit type `(std::sync::mpsc::Sender<T>, std::sync::mpsc::Receiver<T>)`, where
the type parameter `T` is specified

它建議為SenderReceiver添加一個類型。如果你願意的話,可以這樣做:

use std::sync::mpsc::{channel, Sender, Receiver}; // Added Sender and Receiver here

fn main() {
    let (sender, receiver): (Sender<i32>, Receiver<i32>) = channel();
}

但你不必這樣做: 一旦你開始使用SenderReceiver,Rust就能猜到類型。

所以我們來看一下最簡單的使用通道的方法。

use std::sync::mpsc::channel;

fn main() {
    let (sender, receiver) = channel();

    sender.send(5);
    receiver.recv(); // recv = receive, not "rec v"
}

現在編譯器知道類型了。senderResult<(), SendError<i32>>receiverResult<i32, RecvError>。所以你可以用.unwrap()來看看發送是否有效,或者使用更好的錯誤處理。我們加上.unwrap(),也加上println!,看看得到什麼。

use std::sync::mpsc::channel;

fn main() {
    let (sender, receiver) = channel();

    sender.send(5).unwrap();
    println!("{}", receiver.recv().unwrap());
}

這樣就可以打印出5

channel就像Arc一樣,因為你可以克隆它,並將克隆的內容發送到其他線程中。讓我們創建兩個線程,並將值發送到receiver。這段代碼可以工作,但它並不完全是我們想要的。

use std::sync::mpsc::channel;

fn main() {
    let (sender, receiver) = channel();
    let sender_clone = sender.clone();

    std::thread::spawn(move|| { // move sender in
        sender.send("Send a &str this time").unwrap();
    });

    std::thread::spawn(move|| { // move sender_clone in
        sender_clone.send("And here is another &str").unwrap();
    });

    println!("{}", receiver.recv().unwrap());
}

兩個線程開始發送,然後我們println!。它可能會打印 Send a &str this timeAnd here is another &str,這取決於哪個線程先完成。讓我們創建一個join句柄來等待它們完成。

use std::sync::mpsc::channel;

fn main() {
    let (sender, receiver) = channel();
    let sender_clone = sender.clone();
    let mut handle_vec = vec![]; // Put our handles in here

    handle_vec.push(std::thread::spawn(move|| {  // push this into the vec
        sender.send("Send a &str this time").unwrap();
    }));

    handle_vec.push(std::thread::spawn(move|| {  // and push this into the vec
        sender_clone.send("And here is another &str").unwrap();
    }));

    for _ in handle_vec { // now handle_vec has 2 items. Let's print them
        println!("{:?}", receiver.recv().unwrap());
    }
}

這個將打印:

"Send a &str this time"
"And here is another &str"

現在我們不打印,我們創建一個results_vec

use std::sync::mpsc::channel;

fn main() {
    let (sender, receiver) = channel();
    let sender_clone = sender.clone();
    let mut handle_vec = vec![];
    let mut results_vec = vec![];

    handle_vec.push(std::thread::spawn(move|| {
        sender.send("Send a &str this time").unwrap();
    }));

    handle_vec.push(std::thread::spawn(move|| {
        sender_clone.send("And here is another &str").unwrap();
    }));

    for _ in handle_vec {
        results_vec.push(receiver.recv().unwrap());
    }

    println!("{:?}", results_vec);
}

現在結果在我們的vec中:["Send a &str this time", "And here is another &str"]

現在讓我們假設我們有很多工作要做,並且想要使用線程。我們有一個大的VEC,裡面有1百萬個元素,都是0,我們想把每個0都變成1,我們將使用10個線程,每個線程將做十分之一的工作。我們將創建一個新的VEC,並使用.extend()來收集結果。

use std::sync::mpsc::channel;
use std::thread::spawn;

fn main() {
    let (sender, receiver) = channel();
    let hugevec = vec![0; 1_000_000];
    let mut newvec = vec![];
    let mut handle_vec = vec![];

    for i in 0..10 {
        let sender_clone = sender.clone();
        let mut work: Vec<u8> = Vec::with_capacity(hugevec.len() / 10); // new vec to put the work in. 1/10th the size
        work.extend(&hugevec[i*100_000..(i+1)*100_000]); // first part gets 0..100_000, next gets 100_000..200_000, etc.
        let handle =spawn(move || { // make a handle

            for number in work.iter_mut() { // do the actual work
                *number += 1;
            };
            sender_clone.send(work).unwrap(); // use the sender_clone to send the work to the receiver
        });
        handle_vec.push(handle);
    }

    for handle in handle_vec { // stop until the threads are done
        handle.join().unwrap();
    }

    while let Ok(results) = receiver.try_recv() {
        newvec.push(results); // push the results from receiver.recv() into the vec
    }

    // Now we have a Vec<Vec<u8>>. To put it together we can use .flatten()
    let newvec = newvec.into_iter().flatten().collect::<Vec<u8>>(); // Now it's one vec of 1_000_000 u8 numbers

    println!("{:?}, {:?}, total length: {}", // Let's print out some numbers to make sure they are all 1
        &newvec[0..10], &newvec[newvec.len()-10..newvec.len()], newvec.len() // And show that the length is 1_000_000 items
    );

    for number in newvec { // And let's tell Rust that it can panic if even one number is not 1
        if number != 1 {
            panic!();
        }
    }
}

閱讀Rust文檔

知道如何閱讀Rust中的文檔是很重要的,這樣你就可以理解其他人寫的東西。這裡有一些Rust文檔中需要知道的事情。

assert_eq!

你在做測試的時候看到assert_eq!是用的。你把兩個元素放在函數裡面,如果它們不相等,程序就會崩潰。下面是一個簡單的例子,我們需要一個偶數。

fn main() {
    prints_number(56);
}

fn prints_number(input: i32) {
    assert_eq!(input % 2, 0); // number must be equal.
                              // If number % 2 is not 0, it panics
    println!("The number is not odd. It is {}", input);
}

也許你沒有任何計劃在你的代碼中使用assert_eq!,但它在Rust文檔中隨處可見。這是因為在一個文檔中,你需要很大的空間來println!一切。另外,你會需要DisplayDebug來打印你想打印的東西。這就是為什麼文檔中到處都有assert_eq!的原因。下面是這裡的一個例子https://doc.rust-lang.org/std/vec/struct.Vec.html,展示瞭如何使用Vec。

fn main() {
    let mut vec = Vec::new();
    vec.push(1);
    vec.push(2);

    assert_eq!(vec.len(), 2);
    assert_eq!(vec[0], 1);

    assert_eq!(vec.pop(), Some(2));
    assert_eq!(vec.len(), 1);

    vec[0] = 7;
    assert_eq!(vec[0], 7);

    vec.extend([1, 2, 3].iter().copied());

    for x in &vec {
        println!("{}", x);
    }
    assert_eq!(vec, [7, 1, 2, 3]);
}

在這些例子中,你可以只把assert_eq!(a, b)看成是在說 "a是b"。現在看看右邊帶有註釋的同一個例子。註釋顯示了它的實際含義。

fn main() {
    let mut vec = Vec::new();
    vec.push(1);
    vec.push(2);

    assert_eq!(vec.len(), 2); // "The vec length is 2"
    assert_eq!(vec[0], 1); // "vec[0] is 1"

    assert_eq!(vec.pop(), Some(2)); // "When you use .pop(), you get Some()"
    assert_eq!(vec.len(), 1); // "The vec length is now 1"

    vec[0] = 7;
    assert_eq!(vec[0], 7); // "Vec[0] is 7"

    vec.extend([1, 2, 3].iter().copied());

    for x in &vec {
        println!("{}", x);
    }
    assert_eq!(vec, [7, 1, 2, 3]); // "The vec now has [7, 1, 2, 3]"
}

搜索

Rust 文檔的頂部欄是搜索欄。它在你輸入時顯示結果。當你往下翻時,你不能再看到搜索欄,但如果你按鍵盤上的s鍵,你可以再次搜索。所以在任何地方按s鍵可以讓你馬上搜索。

[src] 按鈕

通常一個方法、結構體等的代碼不會是完整的。這是因為你通常不需要看到完整的源碼就能知道它是如何工作的,而完整的代碼可能會讓人困惑。但如果你想知道更多,你可以點擊[src]就可以看到所有的內容。例如,在String的頁面上,你可以看到.with_capacity()的這個簽名。

#![allow(unused)]
fn main() {
// 🚧
pub fn with_capacity(capacity: usize) -> String
}

好了,你輸入一個數字,它給你一個String。這很簡單,但也許我們很好奇,想看更多。如果你點擊[src]你可以看到這個。

#![allow(unused)]
fn main() {
// 🚧
pub fn with_capacity(capacity: usize) -> String {
    String { vec: Vec::with_capacity(capacity) }
}
}

有趣的是,現在你可以看到,字符串是Vec的一種。而實際上一個String是一個u8字節的向量,這很有意思。你不需要知道,就可以使用with_capacity的方法,你只有點擊[src]才能看到。所以如果文檔沒有太多細節,而你又想知道更多的話,點擊[src]是個好主意。

trait信息

trait文檔的重要部分是左邊的 "Required Methods"。如果你看到 "Required Methods",可能意味著你必須自己編寫方法。例如,對於 Iterator,你需要寫 .next() 方法。而對於From,你需要寫.from()方法。但是有些trait只需要一個屬性就可以實現,比如我們在#[derive(Debug)]中看到的。Debug需要.fmt()方法,但通常你只需要使用#[derive(Debug)],除非你想自己做。這就是為什麼在std::fmt::Debug的頁面上說 "一般來說,你應該直接派生出一個Debug實現"。

屬性

你以前見過#[derive(Debug)]這樣的代碼:這種類型的代碼叫做屬性。這些屬性是給編譯器提供信息的小段代碼。它們不容易創建,但使用起來非常方便。如果你只用#寫一個屬性,那麼它將影響下一行的代碼。但如果你用#!來寫,那麼它將影響自己空間裡的一切。

下面是一些你會經常看到的屬性。

#[allow(dead_code)]#[allow(unused_variables)]。 如果你寫了不用的代碼,Rust仍然會編譯,但會讓你知道。例如,這裡有一個結構體,裡面什麼都沒有,只有一個變量。我們不使用它們中的任何一個。

struct JustAStruct {}

fn main() {
    let some_char = 'ん';
}

如果你這樣寫,Rust會提醒你,你沒有使用它們。

warning: unused variable: `some_char`
 --> src\main.rs:4:9
  |
4 |     let some_char = 'ん';
  |         ^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_some_char`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: struct is never constructed: `JustAStruct`
 --> src\main.rs:1:8
  |
1 | struct JustAStruct {}
  |        ^^^^^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

我們知道,可以在名字前寫一個_,讓編譯器安靜下來。

struct _JustAStruct {}

fn main() {
    let _some_char = 'ん';
}

但你也可以使用屬性。你會注意到在消息中,它使用了#[warn(unused_variables)]#[warn(dead_code)]。在我們的代碼中,JustAStruct是死代碼,而some_char是一個未使用的變量。warn的反義詞是allow,所以我們可以這樣寫,它不會說什麼。

#![allow(dead_code)]
#![allow(unused_variables)]

struct Struct1 {} // Create five structs
struct Struct2 {}
struct Struct3 {}
struct Struct4 {}
struct Struct5 {}

fn main() {
    let char1 = 'ん'; // and four variables. We don't use any of them but the compiler is quiet
    let char2 = ';';
    let some_str = "I'm just a regular &str";
    let some_vec = vec!["I", "am", "just", "a", "vec"];
}

當然,處理死代碼和未使用的變量是很重要的。但有時你希望編譯器安靜一段時間。或者您可能需要展示一些代碼或教人們Rust,但又不想用編譯器的信息來迷惑他們。

#[derive(TraitName)]讓你可以為你創建的結構和枚舉派生一些trait。這適用於許多可以自動派生的常見trait。有些像 Display 這樣的特性不能自動衍生,因為對於 Display,你必須選擇如何顯示。

// ⚠️
#[derive(Display)]
struct HoldsAString {
    the_string: String,
}

fn main() {
    let my_string = HoldsAString {
        the_string: "Here I am!".to_string(),
    };
}

錯誤信息會告訴你:

error: cannot find derive macro `Display` in this scope
 --> src\main.rs:2:10
  |
2 | #[derive(Display)]
  |

但是對於可以自動推導出的trait,你可以隨心所欲的放進去。讓我們給HoldsAString在一行中加入七個trait,只是為了好玩,儘管它只需要一個。

#[derive(Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Clone)]
struct HoldsAString {
    the_string: String,
}

fn main() {
    let my_string = HoldsAString {
        the_string: "Here I am!".to_string(),
    };
    println!("{:?}", my_string);
}

另外,如果(也只有在)它的字段都實現了Copy的情況下,你才可以創建一個Copy結構。HoldsAString包含String,它沒有實現Copy,所以你不能對它使用#[derive(Copy)]。但是對下面這個結構你可以:

#[derive(Clone, Copy)] // You also need Clone to use Copy
struct NumberAndBool {
    number: i32, // i32 is Copy
    true_or_false: bool // bool is also Copy. So no problem
}

fn does_nothing(input: NumberAndBool) {

}

fn main() {
    let number_and_bool = NumberAndBool {
        number: 8,
        true_or_false: true
    };

    does_nothing(number_and_bool);
    does_nothing(number_and_bool); // If it didn't have copy, this would make an error
}

#[cfg()]的意思是配置,告訴編譯器是否運行代碼。它通常是這樣的:#[cfg(test)]。你在寫測試函數的時候用這個,這樣它就知道除非你在測試,否則不要運行它們。那麼你可以在你的代碼附近寫測試,但編譯器不會運行它們,除非你告訴編譯器。

還有一個使用cfg的例子是#[cfg(target_os = "windows")]。有了它,你可以告訴編譯器只在Windows,Linux或其他平臺則不能運行代碼。

#![no_std]是一個有趣的屬性,它告訴Rust不要引入標準庫。這意味著你沒有VecString,以及標準庫中的其他任何東西。你會在那些沒有多少內存或空間的小型設備的代碼中看到這個。

你可以在這裡看到更多的屬性。

Box

Box 是 Rust 中一個非常方便的類型。當你使用Box時,你可以把一個類型放在堆上而不是棧上。要創建一個新的 Box,只需使用 Box::new() 並將元素放在裡面即可。

fn just_takes_a_variable<T>(item: T) {} // Takes anything and drops it.

fn main() {
    let my_number = 1; // This is an i32
    just_takes_a_variable(my_number);
    just_takes_a_variable(my_number); // Using this function twice is no problem, because it's Copy

    let my_box = Box::new(1); // This is a Box<i32>
    just_takes_a_variable(my_box.clone()); // Without .clone() the second function would make an error
    just_takes_a_variable(my_box); // because Box is not Copy
}

一開始很難想象在哪裡使用它,但你在Rust中經常使用它。你記得&是用於str的,因為編譯器不知道str的大小:它可以是任何長度。但是&的引用總是相同的長度,所以編譯器可以使用它。Box也是類似的。另外,你也可以在Box上使用*來獲取值,就像使用&一樣。

fn main() {
    let my_box = Box::new(1); // This is a Box<i32>
    let an_integer = *my_box; // This is an i32
    println!("{:?}", my_box);
    println!("{:?}", an_integer);
}

這就是為什麼Box被稱為 "智能指針"的原因,因為它就像&的引用(指針的一種),但可以做更多的事情。

你也可以使用Box來創建裡面有相同結構的結構體。這些結構被稱為遞歸,這意味著在Struct A裡面也許是另一個Struct A,有時你可以使用Box來創建鏈表,儘管這在Rust中並不十分流行。但如果你想創建一個遞歸結構,你可以使用Box。如果你試著不用 Box 會發生什麼:

#![allow(unused)]
fn main() {
struct List {
    item: Option<List>, // ⚠️
}
}

這個簡單的List有一項,可能是Some<List>(另一個列表),也可能是None。因為你可以選擇None,所以它不會永遠遞歸。但是編譯器還是不知道大小。

error[E0072]: recursive type `List` has infinite size
  --> src\main.rs:16:1
   |
16 | struct List {
   | ^^^^^^^^^^^ recursive type has infinite size
17 |     item: Option<List>,
   |     ------------------ recursive without indirection
   |
   = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `List` representable

你可以看到,它甚至建議嘗試Box。所以我們用Box把List包裹起來。

struct List {
    item: Option<Box<List>>,
}
fn main() {}

現在編譯器用List就可以了,因為所有的東西都在Box後面,而且它知道Box的大小。那麼一個非常簡單的列表可能是這樣的:

struct List {
    item: Option<Box<List>>,
}

impl List {
    fn new() -> List {
        List {
            item: Some(Box::new(List { item: None })),
        }
    }
}

fn main() {
    let mut my_list = List::new();
}

即使沒有數據也有點複雜,Rust並不怎麼使用這種類型的模式。這是因為Rust對借用和所有權有嚴格的規定,你知道的。但如果你想啟動一個這樣的列表(鏈表),Box可以幫助你。

Box還可以讓你在上面使用std::mem::drop,因為它在堆上。這有時會很方便。

用Box包裹trait

Box對於返回trait非常有用。你可以像這個例子一樣用泛型函數寫trait:

use std::fmt::Display;

struct DoesntImplementDisplay {}

fn displays_it<T: Display>(input: T) {
    println!("{}", input);
}

fn main() {}

這個只能接受Display的東西,所以它不能接受我們的DoesntImplementDisplay結構。但是它可以接受很多其他的東西,比如String

你也看到了,我們可以使用 impl Trait 來返回其他的trait,或者閉包。Box也可以用類似的方式使用。你可以使用 Box,否則編譯器將不知道值的大小。這個例子表明,trait可以用在任何大小的東西上:

#![allow(dead_code)] // Tell the compiler to be quiet
use std::mem::size_of; // This gives the size of a type

trait JustATrait {} // We will implement this on everything

enum EnumOfNumbers {
    I8(i8),
    AnotherI8(i8),
    OneMoreI8(i8),
}
impl JustATrait for EnumOfNumbers {}

struct StructOfNumbers {
    an_i8: i8,
    another_i8: i8,
    one_more_i8: i8,
}
impl JustATrait for StructOfNumbers {}

enum EnumOfOtherTypes {
    I8(i8),
    AnotherI8(i8),
    Collection(Vec<String>),
}
impl JustATrait for EnumOfOtherTypes {}

struct StructOfOtherTypes {
    an_i8: i8,
    another_i8: i8,
    a_collection: Vec<String>,
}
impl JustATrait for StructOfOtherTypes {}

struct ArrayAndI8 {
    array: [i8; 1000], // This one will be very large
    an_i8: i8,
    in_u8: u8,
}
impl JustATrait for ArrayAndI8 {}

fn main() {
    println!(
        "{}, {}, {}, {}, {}",
        size_of::<EnumOfNumbers>(),
        size_of::<StructOfNumbers>(),
        size_of::<EnumOfOtherTypes>(),
        size_of::<StructOfOtherTypes>(),
        size_of::<ArrayAndI8>(),
    );
}

當我們打印這些東西的size的時候,我們得到2, 3, 32, 32, 1002。所以如果你像下面這樣做的話,會得到一個錯誤:

#![allow(unused)]
fn main() {
// ⚠️
fn returns_just_a_trait() -> JustATrait {
    let some_enum = EnumOfNumbers::I8(8);
    some_enum
}
}

它說:

error[E0746]: return type cannot have an unboxed trait object
  --> src\main.rs:53:30
   |
53 | fn returns_just_a_trait() -> JustATrait {
   |                              ^^^^^^^^^^ doesn't have a size known at compile-time

而這是真的,因為size可以是2,3,32,1002,或者其他任何東西。所以我們把它放在一個Box中。在這裡我們還要加上dyn這個關鍵詞。dyn這個詞告訴你,你說的是一個trait,而不是一個結構體或其他任何東西。

所以你可以把函數改成這樣。

#![allow(unused)]
fn main() {
// 🚧
fn returns_just_a_trait() -> Box<dyn JustATrait> {
    let some_enum = EnumOfNumbers::I8(8);
    Box::new(some_enum)
}
}

現在它工作了,因為在棧上只是一個Box,我們知道Box的大小。

你會經常看到Box<dyn Error>這種形式,因為有時你可能會有多個可能的錯誤。

我們可以快速創建兩個錯誤類型來顯示這一點。要創建一個正式的錯誤類型,你必須為它實現std::error::Error。這部分很容易:只要寫出 impl std::error::Error {}。但錯誤還需要DebugDisplay,這樣才能給出問題的信息。Debug只要加上#[derive(Debug)]就行,很容易,但Display需要.fmt()方法。我們之前做過一次。

代碼是這樣的:

use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct ErrorOne;

impl Error for ErrorOne {} // Now it is an error type with Debug. Time for Display:

impl fmt::Display for ErrorOne {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "You got the first error!") // All it does is write this message
    }
}


#[derive(Debug)] // Do the same thing with ErrorTwo
struct ErrorTwo;

impl Error for ErrorTwo {}

impl fmt::Display for ErrorTwo {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "You got the second error!")
    }
}

// Make a function that just returns a String or an error
fn returns_errors(input: u8) -> Result<String, Box<dyn Error>> { // With Box<dyn Error> you can return anything that has the Error trait

    match input {
        0 => Err(Box::new(ErrorOne)), // Don't forget to put it in a box
        1 => Err(Box::new(ErrorTwo)),
        _ => Ok("Looks fine to me".to_string()), // This is the success type
    }

}

fn main() {

    let vec_of_u8s = vec![0_u8, 1, 80]; // Three numbers to try out

    for number in vec_of_u8s {
        match returns_errors(number) {
            Ok(input) => println!("{}", input),
            Err(message) => println!("{}", message),
        }
    }
}

這將打印:

You got the first error!
You got the second error!
Looks fine to me

如果我們沒有Box<dyn Error>,寫了這個,我們就有問題了。

#![allow(unused)]
fn main() {
// ⚠️
fn returns_errors(input: u8) -> Result<String, Error> {
    match input {
        0 => Err(ErrorOne),
        1 => Err(ErrorTwo),
        _ => Ok("Looks fine to me".to_string()),
    }
}
}

它會告訴你。

21  | fn returns_errors(input: u8) -> Result<String, Error> {
    |                                 ^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time

這並不奇怪,因為我們知道,一個trait可以作用於很多東西,而且它們各自有不同的大小。

默認值和建造者模式

你可以實現 Default trait,給你認為最常見的 structenum 賦值。建造者模式可以很好地與之配合,讓用戶在需要時輕鬆地進行修改。首先我們來看看Default。實際上,Rust中的大多數通用類型已經有Default。它們並不奇怪。0, ""(空字符串), false, 等等。

fn main() {
    let default_i8: i8 = Default::default();
    let default_str: String = Default::default();
    let default_bool: bool = Default::default();

    println!("'{}', '{}', '{}'", default_i8, default_str, default_bool);
}

這將打印'0', '', 'false'

所以Default就像new函數一樣,除了你不需要輸入任何東西。首先我們要創建一個struct,它還沒有實現Default。它有一個new函數,我們用它來創建一個名為Billy的角色,並提供一些統計信息。

struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
}

enum LifeState {
    Alive,
    Dead,
    NeverAlive,
    Uncertain
}

impl Character {
    fn new(name: String, age: u8, height: u32, weight: u32, alive: bool) -> Self {
        Self {
            name,
            age,
            height,
            weight,
            lifestate: if alive { LifeState::Alive } else { LifeState::Dead },
        }
    }
}

fn main() {
    let character_1 = Character::new("Billy".to_string(), 15, 170, 70, true);
}

但也許在我們的世界裡,我們希望大部分角色都叫比利,年齡15歲,身高170,體重70,還活著。我們可以實現Default,這樣我們就可以直接寫Character::default()。它看起來是這樣的:

#[derive(Debug)]
struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
}

#[derive(Debug)]
enum LifeState {
    Alive,
    Dead,
    NeverAlive,
    Uncertain,
}

impl Character {
    fn new(name: String, age: u8, height: u32, weight: u32, alive: bool) -> Self {
        Self {
            name,
            age,
            height,
            weight,
            lifestate: if alive {
                LifeState::Alive
            } else {
                LifeState::Dead
            },
        }
    }
}

impl Default for Character {
    fn default() -> Self {
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
        }
    }
}

fn main() {
    let character_1 = Character::default();

    println!(
        "The character {:?} is {:?} years old.",
        character_1.name, character_1.age
    );
}

打印出The character "Billy" is 15 years old.,簡單多了!

現在我們來看建造者模式。我們會有很多Billy,所以我們會保留默認的。但是很多其他角色只會有一點不同。建造者模式讓我們可以把小方法鏈接起來,每次改變一個值。這裡是一個Character的方法:

#![allow(unused)]
fn main() {
fn height(mut self, height: u32) -> Self {    // 🚧
    self.height = height;
    self
}
}

一定要注意,它取的是mut self。我們之前看到過一次,它不是一個可變引用(&mut self)。它佔用了Self的所有權,有了mut,它將是可變的,即使它之前不是可變的。這是因為.height()擁有完全的所有權,別人不能碰它,所以它是安全的,可變。它只是改變self.height,然後返回Self(就是Character)。

所以我們有三個這樣的構建方法。它們幾乎是一樣的:

#![allow(unused)]
fn main() {
fn height(mut self, height: u32) -> Self {     // 🚧
    self.height = height;
    self
}

fn weight(mut self, weight: u32) -> Self {
    self.weight = weight;
    self
}

fn name(mut self, name: &str) -> Self {
    self.name = name.to_string();
    self
}
}

每一個都會改變一個變量,並回饋給Self:這就是你在建造者模式中看到的。所以現在我們類似這樣寫來創建一個角色:let character_1 = Character::default().height(180).weight(60).name("Bobby");。如果你正在構建一個庫給別人使用,這可以讓他們很容易用起來。對最終用戶來說很容易,因為它幾乎看起來像自然的英語。"給我一個默認角色,身高為180,體重為60,名字為Bobby." 到目前為止,我們的代碼看起來是這樣的:

#[derive(Debug)]
struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
}

#[derive(Debug)]
enum LifeState {
    Alive,
    Dead,
    NeverAlive,
    Uncertain,
}

impl Character {
    fn new(name: String, age: u8, height: u32, weight: u32, alive: bool) -> Self {
        Self {
            name,
            age,
            height,
            weight,
            lifestate: if alive {
                LifeState::Alive
            } else {
                LifeState::Dead
            },
        }
    }

    fn height(mut self, height: u32) -> Self {
        self.height = height;
        self
    }

    fn weight(mut self, weight: u32) -> Self {
        self.weight = weight;
        self
    }

    fn name(mut self, name: &str) -> Self {
        self.name = name.to_string();
        self
    }
}

impl Default for Character {
    fn default() -> Self {
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
        }
    }
}

fn main() {
    let character_1 = Character::default().height(180).weight(60).name("Bobby");

    println!("{:?}", character_1);
}

最後一個要添加的方法通常叫.build()。這個方法是一種最終檢查。當你給用戶提供一個像.height()這樣的方法時,你可以確保他們只輸入一個u32(),但是如果他們輸入5000的身高怎麼辦?這在你正在做的遊戲中可能就不對了。我們最後將使用一個名為.build()的方法,返回一個Result。在它裡面我們將檢查用戶輸入是否正常,如果正常,我們將返回一個 Ok(Self)

不過首先我們要改變.new()方法。我們不希望用戶再自由創建任何一種角色。所以我們將把impl Default的值移到.new()。而現在.new()不接受任何輸入。

#![allow(unused)]
fn main() {
    fn new() -> Self {    // 🚧
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
        }
    }
}

這意味著我們不再需要impl Default了,因為.new()有所有的默認值。所以我們可以刪除impl Default

現在我們的代碼是這樣的。

#[derive(Debug)]
struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
}

#[derive(Debug)]
enum LifeState {
    Alive,
    Dead,
    NeverAlive,
    Uncertain,
}

impl Character {
    fn new() -> Self {
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
        }
    }

    fn height(mut self, height: u32) -> Self {
        self.height = height;
        self
    }

    fn weight(mut self, weight: u32) -> Self {
        self.weight = weight;
        self
    }

    fn name(mut self, name: &str) -> Self {
        self.name = name.to_string();
        self
    }
}

fn main() {
    let character_1 = Character::new().height(180).weight(60).name("Bobby");

    println!("{:?}", character_1);
}

這樣打印出來的結果是一樣的:Character { name: "Bobby", age: 15, height: 180, weight: 60, lifestate: Alive }

我們幾乎已經準備好寫.build()方法了,但是有一個問題:如何讓用戶使用它?現在用戶可以寫let x = Character::new().height(76767);,然後得到一個Character。有很多方法可以做到這一點,也許你能想出自己的方法。但是我們會在Character中增加一個can_use: bool的值。

#![allow(unused)]
fn main() {
#[derive(Debug)]       // 🚧
struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
    can_use: bool, // Set whether the user can use the character
}

\\ Cut other code

    fn new() -> Self {
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
            can_use: true, // .new() always gives a good character, so it's true
        }
    }
}

而對於其他的方法,比如.height(),我們會將can_use設置為false。只有.build()會再次設置為true,所以現在用戶要用.build()做最後的檢查。我們要確保height不高於200,weight不高於300。另外,在我們的遊戲中,有一個不好的字叫smurf,我們不希望任何角色使用它。

我們的.build()方法是這樣的:

#![allow(unused)]
fn main() {
fn build(mut self) -> Result<Character, String> {      // 🚧
    if self.height < 200 && self.weight < 300 && !self.name.to_lowercase().contains("smurf") {
        self.can_use = true;
        Ok(self)
    } else {
        Err("Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)"
            .to_string())
    }
}
}

!self.name.to_lowercase().contains("smurf") 確保用戶不會寫出類似 "SMURF"或 "IamSmurf"的字樣。它讓整個 String 都變成小寫(小字母),並檢查 .contains() 而不是 ==。而前面的!表示 "不是"。

如果一切正常,我們就把can_use設置為true,然後把Ok裡面的字符給用戶。

現在我們的代碼已經完成了,我們將創建三個不工作的角色,和一個工作的角色。最後的代碼是這樣的。

#[derive(Debug)]
struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
    can_use: bool, // Here is the new value
}

#[derive(Debug)]
enum LifeState {
    Alive,
    Dead,
    NeverAlive,
    Uncertain,
}

impl Character {
    fn new() -> Self {
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
            can_use: true,  // .new() makes a fine character, so it is true
        }
    }

    fn height(mut self, height: u32) -> Self {
        self.height = height;
        self.can_use = false; // Now the user can't use the character
        self
    }

    fn weight(mut self, weight: u32) -> Self {
        self.weight = weight;
        self.can_use = false;
        self
    }

    fn name(mut self, name: &str) -> Self {
        self.name = name.to_string();
        self.can_use = false;
        self
    }

    fn build(mut self) -> Result<Character, String> {
        if self.height < 200 && self.weight < 300 && !self.name.to_lowercase().contains("smurf") {
            self.can_use = true;   // Everything is okay, so set to true
            Ok(self)               // and return the character
        } else {
            Err("Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)"
                .to_string())
        }
    }
}

fn main() {
    let character_with_smurf = Character::new().name("Lol I am Smurf!!").build(); // This one contains "smurf" - not okay
    let character_too_tall = Character::new().height(400).build(); // Too tall - not okay
    let character_too_heavy = Character::new().weight(500).build(); // Too heavy - not okay
    let okay_character = Character::new()
        .name("Billybrobby")
        .height(180)
        .weight(100)
        .build();   // This character is okay. Name is fine, height and weight are fine

    // Now they are not Character, they are Result<Character, String>. So let's put them in a Vec so we can see them:
    let character_vec = vec![character_with_smurf, character_too_tall, character_too_heavy, okay_character];

    for character in character_vec { // Now we will print the character if it's Ok, and print the error if it's Err
        match character {
            Ok(character_info) => println!("{:?}", character_info),
            Err(err_info) => println!("{}", err_info),
        }
        println!(); // Then add one more line
    }
}

這將打印:

Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)

Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)

Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)

Character { name: "Billybrobby", age: 15, height: 180, weight: 100, lifestate: Alive, can_use: true }

Deref和DerefMut

Deref是讓你用*來解引用某些東西的trait。我們知道,一個引用和一個值是不一樣的。

// ⚠️
fn main() {
    let value = 7; // This is an i32
    let reference = &7; // This is a &i32
    println!("{}", value == reference);
}

而Rust連false都不給,因為它甚至不會比較兩者。

error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src\main.rs:4:26
  |
4 |     println!("{}", value == reference);
  |                          ^^ no implementation for `{integer} == &{integer}`

當然,這裡的解法是使用*。所以這將打印出true

fn main() {
    let value = 7;
    let reference = &7;
    println!("{}", value == *reference);
}

現在讓我們想象一下一個簡單的類型,它只是容納一個數字。它就像一個Box,我們有一些想法為它提供一些額外的功能。但如果我們只是給它一個數字, 它就不能做那麼多了。

我們不能像使用Box那樣使用*:

// ⚠️
struct HoldsANumber(u8);

fn main() {
    let my_number = HoldsANumber(20);
    println!("{}", *my_number + 20);
}

錯誤信息是:

error[E0614]: type `HoldsANumber` cannot be dereferenced
  --> src\main.rs:24:22
   |
24 |     println!("{:?}", *my_number + 20);

我們當然可以做到這一點。println!("{:?}", my_number.0 + 20);. 但是這樣的話,我們就是在20的基礎上再單獨加一個u8。如果我們能把它們加在一起就更好了。cannot be dereferenced這個消息給了我們一個線索:我們需要實現Deref。實現Deref的簡單東西有時被稱為 "智能指針"。一個智能指針可以指向它的元素,有它的信息,並且可以使用它的方法。因為現在我們可以添加my_number.0,這是一個u8,但我們不能用HoldsANumber做其他的事情:到目前為止,它只有Debug

有趣的是:String其實是&str的智能指針,Vec是數組(或其他類型)的智能指針。所以我們其實從一開始就在使用智能指針。

實現Deref並不難,標準庫中的例子也很簡單。下面是標準庫中的示例代碼

use std::ops::Deref;

struct DerefExample<T> {
    value: T
}

impl<T> Deref for DerefExample<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

fn main() {
    let x = DerefExample { value: 'a' };
    assert_eq!('a', *x);
}

所以我們按照這個來,現在我們的Deref是這樣的。

#![allow(unused)]
fn main() {
// 🚧
impl Deref for HoldsANumber {
    type Target = u8; // Remember, this is the "associated type": the type that goes together.
                      // You have to use the right type Target = (the type you want to return)

    fn deref(&self) -> &Self::Target { // Rust calls .deref() when you use *. We just defined Target as a u8 so this is easy to understand
        &self.0   // We chose &self.0 because it's a tuple struct. In a named struct it would be something like "&self.number"
    }
}
}

所以現在我們可以用*來做:

use std::ops::Deref;
#[derive(Debug)]
struct HoldsANumber(u8);

impl Deref for HoldsANumber {
    type Target = u8;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let my_number = HoldsANumber(20);
    println!("{:?}", *my_number + 20);
}

所以,這樣就可以打印出40,我們不需要寫my_number.0。這意味著我們得到了 u8 的方法,我們可以為 HoldsANumber 寫出我們自己的方法。我們將添加自己的簡單方法,並使用我們從u8中得到的另一個方法,稱為.checked_sub().checked_sub()方法是一個安全的減法,它能返回一個Option。如果它能做減法,那麼它就會在Some裡面給你,如果它不能做減法,那麼它就會給出一個None。記住,u8不能是負數,所以還是.checked_sub()比較安全,這樣就不會崩潰了。

use std::ops::Deref;

struct HoldsANumber(u8);

impl HoldsANumber {
    fn prints_the_number_times_two(&self) {
        println!("{}", self.0 * 2);
    }
}

impl Deref for HoldsANumber {
    type Target = u8;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let my_number = HoldsANumber(20);
    println!("{:?}", my_number.checked_sub(100)); // This method comes from u8
    my_number.prints_the_number_times_two(); // This is our own method
}

這個打印:

None
40

我們也可以實現DerefMut,這樣我們就可以通過*來改變數值。它看起來幾乎是一樣的。在實現DerefMut之前,你需要先實現Deref

use std::ops::{Deref, DerefMut};

struct HoldsANumber(u8);

impl HoldsANumber {
    fn prints_the_number_times_two(&self) {
        println!("{}", self.0 * 2);
    }
}

impl Deref for HoldsANumber {
    type Target = u8;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DerefMut for HoldsANumber { // You don't need type Target = u8; here because it already knows thanks to Deref
    fn deref_mut(&mut self) -> &mut Self::Target { // Everything else is the same except it says mut everywhere
        &mut self.0
    }
}

fn main() {
    let mut my_number = HoldsANumber(20);
    *my_number = 30; // DerefMut lets us do this
    println!("{:?}", my_number.checked_sub(100));
    my_number.prints_the_number_times_two();
}

所以你可以看到,Deref給你的類型提供了強大的力量。

這也是為什麼標準庫說:Deref should only be implemented for smart pointers to avoid confusion。這是因為對於一個複雜的類型,你可以用 Deref 做一些奇怪的事情。讓我們想象一個非常混亂的例子來理解它們的含義。我們將從一個遊戲的 Character 結構開始。一個新的Character需要一些數據,比如智力和力量。所以這裡是我們的第一個角色。

struct Character {
    name: String,
    strength: u8,
    dexterity: u8,
    health: u8,
    intelligence: u8,
    wisdom: u8,
    charm: u8,
    hit_points: i8,
    alignment: Alignment,
}

impl Character {
    fn new(
        name: String,
        strength: u8,
        dexterity: u8,
        health: u8,
        intelligence: u8,
        wisdom: u8,
        charm: u8,
        hit_points: i8,
        alignment: Alignment,
    ) -> Self {
        Self {
            name,
            strength,
            dexterity,
            health,
            intelligence,
            wisdom,
            charm,
            hit_points,
            alignment,
        }
    }
}

enum Alignment {
    Good,
    Neutral,
    Evil,
}

fn main() {
    let billy = Character::new("Billy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alignment::Good);
}

現在讓我們想象一下,我們要把人物的hit points放在一個大的vec裡。也許我們會把怪物數據也放進去,把它放在一起。由於 hit_points 是一個 i8,我們實現了 Deref,所以我們可以對它進行各種計算。但是看看現在我們的main()函數中,它看起來多麼奇怪。

use std::ops::Deref;

// All the other code is the same until after the enum Alignment
struct Character {
    name: String,
    strength: u8,
    dexterity: u8,
    health: u8,
    intelligence: u8,
    wisdom: u8,
    charm: u8,
    hit_points: i8,
    alignment: Alignment,
}

impl Character {
    fn new(
        name: String,
        strength: u8,
        dexterity: u8,
        health: u8,
        intelligence: u8,
        wisdom: u8,
        charm: u8,
        hit_points: i8,
        alignment: Alignment,
    ) -> Self {
        Self {
            name,
            strength,
            dexterity,
            health,
            intelligence,
            wisdom,
            charm,
            hit_points,
            alignment,
        }
    }
}

enum Alignment {
    Good,
    Neutral,
    Evil,
}

impl Deref for Character { // impl Deref for Character. Now we can do any integer math we want!
    type Target = i8;

    fn deref(&self) -> &Self::Target {
        &self.hit_points
    }
}



fn main() {
    let billy = Character::new("Billy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alignment::Good); // Create two characters, billy and brandy
    let brandy = Character::new("Brandy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alignment::Good);

    let mut hit_points_vec = vec![]; // Put our hit points data in here
    hit_points_vec.push(*billy);     // Push *billy?
    hit_points_vec.push(*brandy);    // Push *brandy?

    println!("{:?}", hit_points_vec);
}

這隻打印了[5, 5]。我們的代碼現在讓人讀起來感覺非常奇怪。我們可以在main()上面看到Deref,然後弄清楚*billy的意思是i8,但是如果有很多代碼呢?可能我們的代碼有2000行,突然要弄清楚為什麼要.push() *billyCharacter當然不僅僅是i8的智能指針。

當然,寫hit_points_vec.push(*billy)並不違法,但這讓代碼看起來非常奇怪。也許一個簡單的.get_hp()方法會好得多,或者另一個存放角色的結構體。然後你可以迭代並推送每個角色的 hit_pointsDeref提供了很多功能,但最好確保代碼的邏輯性。

Crate和模塊

每次你在 Rust 中寫代碼時,你都是在 crate 中寫的。crate是一個或多個文件,一起為你的代碼服務。在你寫的文件裡面,你也可以創建一個modmod是存放函數、結構體等的空間,因為這些原因被使用:

  • 構建你的代碼:它可以幫助你思考代碼的總體結構。當你的代碼越來越大時,這一點可能很重要。
  • 閱讀你的代碼:人們可以更容易理解你的代碼。例如,std::collections::HashMap這個名字告訴你,它在std的模塊collections裡面。這給了你一個提示,也許collections裡面還有更多的集合類型,你可以嘗試一下。
  • 私密性:所有的東西一開始都是私有的。這樣可以讓你不讓用戶直接使用函數。

要創建一個mod,只需要寫mod,然後用{}開始一個代碼塊。我們將創建一個名為print_things的mod,它有一些打印相關的功能。

mod print_things {
    use std::fmt::Display;

    fn prints_one_thing<T: Display>(input: T) { // Print anything that implements Display
        println!("{}", input)
    }
}

fn main() {}

你可以看到,我們把use std::fmt::Display;寫在print_things裡面,因為它是一個獨立的空間。如果你把use std::fmt::Display;寫在main()裡面,那沒用。而且,我們現在也不能從main()裡面調用。如果在fn前面沒有pub這個關鍵字,它就會保持私密性。讓我們試著在沒有pub的情況下調用它。這裡有一種寫法。

// 🚧
fn main() {
    crate::print_things::prints_one_thing(6);
}

crate的意思是 "在這個項目裡",但對於我們簡單的例子來說,它和 "在這個文件裡面"是一樣的。接著是print_things這個mod,最後是prints_one_thing()函數。你可以每次都寫這個,也可以寫use來導入。現在我們可以看到說它是私有的錯誤:

// ⚠️
mod print_things {
    use std::fmt::Display;

    fn prints_one_thing<T: Display>(input: T) {
        println!("{}", input)
    }
}

fn main() {
    use crate::print_things::prints_one_thing;

    prints_one_thing(6);
    prints_one_thing("Trying to print a string...".to_string());
}

這是錯誤的。

error[E0603]: function `prints_one_thing` is private
  --> src\main.rs:10:30
   |
10 |     use crate::print_things::prints_one_thing;
   |                              ^^^^^^^^^^^^^^^^ private function
   |
note: the function `prints_one_thing` is defined here
  --> src\main.rs:4:5
   |
4  |     fn prints_one_thing<T: Display>(input: T) {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

很容易理解,函數print_one_thing是私有的。它還用src\main.rs:4:5告訴我們在哪裡可以找到這個函數。這很有幫助,因為你不僅可以在一個文件中寫mod,還可以在很多文件中寫mod

現在我們只需要寫pub fn而不是fn,一切就都可以了。

mod print_things {
    use std::fmt::Display;

    pub fn prints_one_thing<T: Display>(input: T) {
        println!("{}", input)
    }
}

fn main() {
    use crate::print_things::prints_one_thing;

    prints_one_thing(6);
    prints_one_thing("Trying to print a string...".to_string());
}

這個打印:

6
Trying to print a string...

pub對結構體、枚舉、trait或模塊有什麼作用?pub對它們來說是這樣的:

  • pub對於一個結構:它使結構公開,但成員不是公開的。要想讓一個成員公開,你也要為每個成員寫pub
  • pub 對於一個枚舉或trait:所有的東西都變成了公共的。這是有意義的,因為traits是給事物賦予相同的行為。而枚舉是值之間的選擇,你需要看到所有的枚舉值才能做選擇。
  • pub對於一個模塊來說:一個頂層的模塊會是pub,因為如果它不是pub,那麼根本沒有人可以使用裡面的任何東西。但是模塊裡面的模塊需要使用pub才能成為公共的。

我們在print_things裡面放一個名為Billy的結構體。這個結構體幾乎都會是public的,但也不盡然。這個結構是公共的,所以它這樣寫:pub struct Billy。裡面會有一個 nametimes_to_printname不會是公共的,因為我們只想讓用戶創建名為"Billy".to_string()的結構。但是用戶可以選擇打印的次數,所以這將是公開的。它的是這樣的:

mod print_things {
    use std::fmt::{Display, Debug};

    #[derive(Debug)]
    pub struct Billy { // Billy is public
        name: String, // but name is private.
        pub times_to_print: u32,
    }

    impl Billy {
        pub fn new(times_to_print: u32) -> Self { // That means the user needs to use new to create a Billy. The user can only change the number of times_to_print
            Self {
                name: "Billy".to_string(), // We choose the name - the user can't
                times_to_print,
            }
        }

        pub fn print_billy(&self) { // This function prints a Billy
            for _ in 0..self.times_to_print {
                println!("{:?}", self.name);
            }
        }
    }

    pub fn prints_one_thing<T: Display>(input: T) {
        println!("{}", input)
    }
}

fn main() {
    use crate::print_things::*; // Now we use *. This imports everything from print_things

    let my_billy = Billy::new(3);
    my_billy.print_billy();
}

這將打印:

"Billy"
"Billy"
"Billy"

對了,導入一切的*叫做 "glob運算符"。Glob的意思是 "全局",所以它意味著一切。

mod裡面你可以創建其他mod。一個子 mod(mod裡的mod)總是可以使用父 mod 內部的任何東西。你可以在下一個例子中看到這一點,我們在 mod province 裡面有一個 mod city,而mod provincemod country 裡面。

你可以這樣想:即使你在一個國家,你可能不在一個省。而即使你在一個省,你也可能不在一個市。但如果你在一個城市,你就在這個城市的省份和它的國家。

mod country { // The main mod doesn't need pub
    fn print_country(country: &str) { // Note: this function isn't public
        println!("We are in the country of {}", country);
    }
    pub mod province { // Make this mod public

        fn print_province(province: &str) { // Note: this function isn't public
            println!("in the province of {}", province);
        }

        pub mod city { // Make this mod public
            pub fn print_city(country: &str, province: &str, city: &str) {  // This function is public though
                crate::country::print_country(country);
                crate::country::province::print_province(province);
                println!("in the city of {}", city);
            }
        }
    }
}

fn main() {
    crate::country::province::city::print_city("Canada", "New Brunswick", "Moncton");
}

有趣的是,print_city可以訪問print_provinceprint_country。這是因為mod city在其他mod裡面。它不需要在print_province前面添加pub之後才能使用。這也是有道理的:一個城市不需要做什麼,它本來就在一個省裡,在一個國家裡。

你可能注意到,crate::country::province::print_province(province);非常長。當我們在一個模塊裡面的時候,我們可以用super從上面引入元素。其實super這個詞本身就是"上面"的意思,比如 "上級"。在我們的例子中,我們只用了一次函數,但是如果你用的比較多的話,那麼最好是導入。如果它能讓你的代碼更容易閱讀,那也是個好主意,即使你只用了一次函數。現在的代碼幾乎是一樣的,但更容易閱讀一些。

mod country {
    fn print_country(country: &str) {
        println!("We are in the country of {}", country);
    }
    pub mod province {
        fn print_province(province: &str) {
            println!("in the province of {}", province);
        }

        pub mod city {
            use super::super::*; // use everything in "above above": that means mod country
            use super::*;        // use everything in "above": that means mod province

            pub fn print_city(country: &str, province: &str, city: &str) {
                print_country(country);
                print_province(province);
                println!("in the city of {}", city);
            }
        }
    }
}

fn main() {
    use crate::country::province::city::print_city; // bring in the function

    print_city("Canada", "New Brunswick", "Moncton");
    print_city("Korea", "Gyeonggi-do", "Gwangju"); // Now it's less work to use it again
}

測試

現在我們已經瞭解了模塊,就可以談談測試了。在Rust中測試你的代碼是非常容易的,因為你可以在你的代碼旁邊寫測試。

開始測試的最簡單的方法是在一個函數上面添加#[test]。下面是一個簡單的例子。

#![allow(unused)]
fn main() {
#[test]
fn two_is_two() {
    assert_eq!(2, 2);
}
}

但如果你試圖在playground中運行它,它給出了一個錯誤。error[E0601]: `main` function not found in crate `playground. 這是因為你不使用 Run 來進行測試,你使用 Test 。另外,你不使用 main() 函數進行測試 - 它們在外面運行。要在Playground中運行這個,點擊 RUN 旁邊的···,然後把它改為 Test 。現在如果你點擊它,它將運行測試。(如果你已經安裝了 Rust,你將輸入 cargo test 來做這個測試)

這裡是輸出:

running 1 test
test two_is_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

讓我們把assert_eq!(2, 2)改成assert_eq!(2, 3),看看會有什麼結果。當測試失敗時,你會得到更多的信息。

running 1 test
test two_is_two ... FAILED

failures:

---- two_is_two stdout ----
thread 'two_is_two' panicked at 'assertion failed: `(left == right)`
  left: `2`,
 right: `3`', src/lib.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    two_is_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

assert_eq!(left, right)是Rust中測試一個函數的主要方法。如果它不工作,它將顯示不同的值:左邊有2,但右邊有3。

RUST_BACKTRACE=1是什麼意思?這是計算機上的一個設置,可以提供更多關於錯誤的信息。幸好playground也有:點擊STABLE旁邊的···,然後設置回溯為ENABLED。如果你這樣做,它會給你很多的信息。

running 1 test
test two_is_two ... FAILED

failures:

---- two_is_two stdout ----
thread 'two_is_two' panicked at 'assertion failed: 2 == 3', src/lib.rs:3:5
stack backtrace:
   0: backtrace::backtrace::libunwind::trace
             at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/libunwind.rs:86
   1: backtrace::backtrace::trace_unsynchronized
             at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/mod.rs:66
   2: std::sys_common::backtrace::_print_fmt
             at src/libstd/sys_common/backtrace.rs:78
   3: <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt
             at src/libstd/sys_common/backtrace.rs:59
   4: core::fmt::write
             at src/libcore/fmt/mod.rs:1076
   5: std::io::Write::write_fmt
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/io/mod.rs:1537
   6: std::io::impls::<impl std::io::Write for alloc::boxed::Box<W>>::write_fmt
             at src/libstd/io/impls.rs:176
   7: std::sys_common::backtrace::_print
             at src/libstd/sys_common/backtrace.rs:62
   8: std::sys_common::backtrace::print
             at src/libstd/sys_common/backtrace.rs:49
   9: std::panicking::default_hook::{{closure}}
             at src/libstd/panicking.rs:198
  10: std::panicking::default_hook
             at src/libstd/panicking.rs:215
  11: std::panicking::rust_panic_with_hook
             at src/libstd/panicking.rs:486
  12: std::panicking::begin_panic
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panicking.rs:410
  13: playground::two_is_two
             at src/lib.rs:3
  14: playground::two_is_two::{{closure}}
             at src/lib.rs:2
  15: core::ops::function::FnOnce::call_once
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libcore/ops/function.rs:232
  16: <alloc::boxed::Box<F> as core::ops::function::FnOnce<A>>::call_once
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/liballoc/boxed.rs:1076
  17: <std::panic::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panic.rs:318
  18: std::panicking::try::do_call
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panicking.rs:297
  19: std::panicking::try
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panicking.rs:274
  20: std::panic::catch_unwind
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panic.rs:394
  21: test::run_test_in_process
             at src/libtest/lib.rs:541
  22: test::run_test::run_test_inner::{{closure}}
             at src/libtest/lib.rs:450
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.


failures:
    two_is_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

除非你真的找不到問題所在,否則你不需要使用回溯。但幸運的是你也不需要全部理解。 如果你繼續閱讀,你最終會看到第13行,那裡寫著playground--那是它提到的你的代碼的位置。其他的都是關於Rust為了運行你的程序,在其他庫中所做的事情。但是這兩行告訴你,它看的是playground的第2行和第3行,這是一個提示,要檢查那裡。這裡是那個部分:

  13: playground::two_is_two
             at src/lib.rs:3
  14: playground::two_is_two::{{closure}}
             at src/lib.rs:2

編輯:Rust在2021年初改進了其回溯信息,只顯示最有意義的信息。現在它更容易閱讀。

failures:

---- two_is_two stdout ----
thread 'two_is_two' panicked at 'assertion failed: `(left == right)`
  left: `2`,
 right: `3`', src/lib.rs:3:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/std/src/panicking.rs:493:5
   1: core::panicking::panic_fmt
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/core/src/panicking.rs:92:14
   2: playground::two_is_two
             at ./src/lib.rs:3:5
   3: playground::two_is_two::{{closure}}
             at ./src/lib.rs:2:1
   4: core::ops::function::FnOnce::call_once
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/core/src/ops/function.rs:227:5
   5: core::ops::function::FnOnce::call_once
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/core/src/ops/function.rs:227:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.


failures:
    two_is_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s

現在我們再把回溯關閉,回到常規測試。現在我們要寫一些其他函數,並使用測試函數來測試它們。這裡有幾個:

#![allow(unused)]
fn main() {
fn return_two() -> i8 {
    2
}
[test]
fn it_returns_two() {
    assert_eq!(return_two(), 2);
}

fn return_six() -> i8 {
    4 + return_two()
}
[test]
fn it_returns_six() {
    assert_eq!(return_six(), 6)
}
}

現在,都能運行:

running 2 tests
test it_returns_two ... ok
test it_returns_six ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

這不是太難。

通常你會想把你的測試放在自己的模塊中。要做到這一點,請使用相同的 mod 關鍵字,並在其上方添加 #[cfg(test)](記住:cfg 的意思是 "配置")。你還要在每個測試上面繼續寫#[test]。這是因為以後當你安裝Rust時,你可以做更復雜的測試。你將可以運行一個測試,或者所有的測試,或者運行幾個測試。另外別忘了寫use super::*;,因為測試模塊需要使用上面的函數。現在它看起來會是這樣的。

#![allow(unused)]
fn main() {
fn return_two() -> i8 {
    2
}
fn return_six() -> i8 {
    4 + return_two()
}

[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_returns_six() {
        assert_eq!(return_six(), 6)
    }
    #[test]
    fn it_returns_two() {
        assert_eq!(return_two(), 2);
    }
}
}

測試驅動的開發

在閱讀Rust或其他語言時,你可能會看到 "測試驅動開發"這個詞。這是編寫程序的一種方式,有些人喜歡它,而有些人則喜歡其他的方式。"測試驅動開發"的意思是 "先寫測試,再寫代碼"。當你這樣做的時候,你會有很多關於你想要你的代碼做的所有事情的測試代碼。然後你開始寫代碼,並運行測試,看看你是否做對了。然後,當你添加和重寫代碼時,如果有什麼地方出了問題,測試代碼會一直在那裡向你展示。這在Rust中是非常容易的,因為編譯器給出了很多待修復內容的信息。讓我們寫一個測試驅動開發的小例子,看看它是什麼樣子的。

讓我們想象一個接受用戶輸入的計算器。它可以加(+),也可以減(-)。如果用戶寫 "5+6",它應該返回11,如果用戶寫 "5+6-7",它應該返回4,以此類推。所以我們先從測試函數開始。你也可以看到,測試中的函數名通常都相當長。這是因為你可能會運行很多測試,你想了解哪些測試失敗了。

我們想象一下,一個名為math()的函數就可以完成所有的工作。它將返回一個 i32(我們不會使用浮點數)。因為它需要返回一些東西,所以我們每次都只返回 6。然後我們將寫三個測試函數。當然,它們都會失敗。現在的代碼是這樣的。

#![allow(unused)]
fn main() {
fn math(input: &str) -> i32 {
    6
}

[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
}
}

它給了我們這個信息。

running 3 tests
test tests::one_minus_minus_one_is_two ... FAILED
test tests::one_minus_two_is_minus_one ... FAILED
test tests::one_plus_one_is_two ... FAILED

以及thread 'tests::one_plus_one_is_two' panicked at 'assertion failed: `(left == right)` 的所有信息。我們不需要在這裡全部打印出來。

現在要考慮如何創建計算器。我們將接受任何數字,以及符號+-。我們將允許空格,但不允許其他任何東西。所以,讓我們從包含所有數值的const開始。然後我們將使用 .chars() 按字符進行迭代,並使用 .all() 確保它們都在裡面。

然後,我們將添加一個會崩潰的測試。要做到這一點,添加 #[should_panic] 屬性:現在如果它崩潰,測試將成功。

現在代碼看起來像這樣:

#![allow(unused)]
fn main() {
const OKAY_CHARACTERS: &str = "1234567890+- "; // Don't forget the space at the end

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) {
        panic!("Please only input numbers, +-, or spaces");
    }
    6 // we still return a 6 for now
}

[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }

    #[test]
    #[should_panic]  // Here is our new test - it should panic
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

現在,當我們運行測試時,我們得到這樣的結果。

running 4 tests
test tests::one_minus_two_is_minus_one ... FAILED
test tests::one_minus_minus_one_is_two ... FAILED
test tests::panics_when_characters_not_right ... ok
test tests::one_plus_one_is_two ... FAILED

一個成功了! 我們的math()函數現在只能接受好的輸入了。

下一步是編寫實際的計算器。這就是先有測試的有趣之處:實際的代碼要晚很多。首先,我們將把計算器的邏輯放在一起。我們要做到以下幾點。

  • 所有的空位都應該被刪除。這在.filter()中很容易實現。
  • 所有輸入應該變成一個Vec+不需要成為輸入,但是當程序看到+時,應該知道這個數字已經完成了。例如,輸入+應該這樣做:
    1. 看到1,把它推到一個空字符串中。
    2. 看到另一個1,把它推入字符串中(現在是 "11")。
    3. 看到一個+,知道這個數字已經結束。它會把字符串推入vec中,然後清空字符串。
  • 程序必須計算出-的數量。奇數(1,3,5...)表示減法,偶數(2,4,6...)表示加法。所以 "1--9"應該是10,而不是-8。
  • 程序應該刪除最後一個數字後面的任何東西。5+5+++++----是由OKAY_CHARACTERS中的所有字符組成的,但它應該變成5+5.trim_end_matches()就很簡單了,你把&str末尾符合的東西都去掉。

順便說一下,.trim_end_matches().trim_start_matches()曾經是trim_right_matches()trim_left_matches()。但後來人們注意到有些語言是從右到左(波斯語、希伯來語等),所以左右都是錯的。你可能還能在一些代碼中看到舊的名字,但它們是一樣的)。)

首先我們只想通過所有的測試。通過測試後,我們就可以 "重構"了。重構的意思是讓代碼變得更好,通常是通過結構、枚舉和方法等方式。下面是我們使測試通過的代碼。

#![allow(unused)]
fn main() {
const OKAY_CHARACTERS: &str = "1234567890+- ";

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) ||
       !input.chars().take(2).any(|character| character.is_numeric())
    {
        panic!("Please only input numbers, +-, or spaces.");
    }

    let input = input.trim_end_matches(|x| "+- ".contains(x)).chars().filter(|x| *x != ' ').collect::<String>(); // Remove + and - at the end, and all spaces
    let mut result_vec = vec![]; // Results go in here
    let mut push_string = String::new(); // This is the string we push in every time. We will keep reusing it in the loop.
    for character in input.chars() {
        match character {
            '+' => {
                if !push_string.is_empty() { // If the string is empty, we don't want to push "" into result_vec
                    result_vec.push(push_string.clone()); // But if it's not empty, it will be a number. Push it into the vec
                    push_string.clear(); // Then clear the string
                }
            },
            '-' => { // If we get a -,
                if push_string.contains('-') || push_string.is_empty() { // check to see if it's empty or has a -
                    push_string.push(character) // if so, then push it in
                } else { // otherwise, it will contain a number
                result_vec.push(push_string.clone()); // so push the number into result_vec, clear it and then push -
                push_string.clear();
                push_string.push(character);
                }
            },
            number => { // number here means "anything else that matches". We selected the name here
                if push_string.contains('-') { // We might have some - characters to push in first
                    result_vec.push(push_string.clone());
                    push_string.clear();
                    push_string.push(number);
                } else { // But if we don't, that means we can push the number in
                    push_string.push(number);
                }
            },
        }
    }
    result_vec.push(push_string); // Push one last time after the loop is over. Don't need to .clone() because we don't use it anymore

    let mut total = 0; // Now it's time to do math. Start with a total
    let mut adds = true; // true = add, false = subtract
    let mut math_iter = result_vec.into_iter();
    while let Some(entry) = math_iter.next() { // Iter through the items
        if entry.contains('-') { // If it has a - character, check if it's even or odd
            if entry.chars().count() % 2 == 1 {
                adds = match adds {
                    true => false,
                    false => true
                };
                continue; // Go to the next item
            } else {
                continue;
            }
        }
        if adds == true {
            total += entry.parse::<i32>().unwrap(); // If there is no '-', it must be a number. So we are safe to unwrap
        } else {
            total -= entry.parse::<i32>().unwrap();
            adds = true;  // After subtracting, reset adds to true.
        }
    }
    total // Finally, return the total
}
   /// We'll add a few more tests just to make sure

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
    #[test]
    fn nine_plus_nine_minus_nine_minus_nine_is_zero() {
        assert_eq!(math("9+9-9-9"), 0); // This is a new test
    }
    #[test]
    fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
        assert_eq!(math("8  - 9     +9-----+++++"), 8); // This is a new test
    }
    #[test]
    #[should_panic]
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

現在測試通過了!

running 6 tests
test tests::one_minus_minus_one_is_two ... ok
test tests::nine_plus_nine_minus_nine_minus_nine_is_zero ... ok
test tests::one_minus_two_is_minus_one ... ok
test tests::eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end ... ok
test tests::one_plus_one_is_two ... ok
test tests::panics_when_characters_not_right ... ok

test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

你可以看到,在測試驅動的開發中,有一個來回的過程。它是這樣的。

  • 首先你要寫出所有你能想到的測試
  • 然後你開始寫代碼。
  • 當你寫代碼的時候,你會有其他測試的想法。
  • 你添加測試,你的測試隨著你的發展而增長。你的測試越多,你的代碼被檢查的次數就越多。

當然,測試並不能檢查所有的東西,認為 "通過所有測試=代碼是完美的"是錯誤的。但是,測試對於你修改代碼的時候是非常好的。如果你以後修改了代碼,然後運行測試,如果其中一個測試不成功,你就會知道該怎麼修復。

現在我們可以重寫(重構)一下代碼。一個好的方法是用clippy開始。如果你安裝了Rust,那麼你可以輸入cargo clippy,如果你使用的是Playground,那麼點擊TOOLS,選擇Clippy。Clippy會查看你的代碼,並給你提示,讓你的代碼更簡單。我們的代碼沒有任何錯誤,但它可以更好。

Clippy會告訴我們兩件事。

warning: this loop could be written as a `for` loop
  --> src/lib.rs:44:5
   |
44 |     while let Some(entry) = math_iter.next() { // Iter through the items
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `for entry in math_iter`
   |
   = note: `#[warn(clippy::while_let_on_iterator)]` on by default
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#while_let_on_iterator

warning: equality checks against true are unnecessary
  --> src/lib.rs:53:12
   |
53 |         if adds == true {
   |            ^^^^^^^^^^^^ help: try simplifying it as shown: `adds`
   |
   = note: `#[warn(clippy::bool_comparison)]` on by default
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#bool_comparison

這是真的:for entry in math_iterwhile let Some(entry) = math_iter.next()簡單得多。而for循環實際上是一個迭代器,所以我們沒有任何理由寫.iter()。謝謝你,clippy! 而且我們也不需要做math_iter:我們可以直接寫for entry in result_vec

現在我們將開始一些真正的重構。我們將創建一個 Calculator 結構體,而不是單獨的變量。這將擁有我們使用的所有變量。我們將改變兩個名字以使其更加清晰。result_vec將變成resultspush_string將變成current_input(current的意思是 "現在")。而到目前為止,它只有一種方法:new。

#![allow(unused)]
fn main() {
// 🚧
#[derive(Clone)]
struct Calculator {
    results: Vec<String>,
    current_input: String,
    total: i32,
    adds: bool,
}

impl Calculator {
    fn new() -> Self {
        Self {
            results: vec![],
            current_input: String::new(),
            total: 0,
            adds: true,
        }
    }
}
}

現在我們的代碼其實比較長,但更容易讀懂。比如,if adds現在是if calculator.adds,這就跟讀英文完全一樣。它的樣子是這樣的:

#![allow(unused)]
fn main() {
#[derive(Clone)]
struct Calculator {
    results: Vec<String>,
    current_input: String,
    total: i32,
    adds: bool,
}

impl Calculator {
    fn new() -> Self {
        Self {
            results: vec![],
            current_input: String::new(),
            total: 0,
            adds: true,
        }
    }
}

const OKAY_CHARACTERS: &str = "1234567890+- ";

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) ||
       !input.chars().take(2).any(|character| character.is_numeric()) {
        panic!("Please only input numbers, +-, or spaces");
    }

    let input = input.trim_end_matches(|x| "+- ".contains(x)).chars().filter(|x| *x != ' ').collect::<String>();
    let mut calculator = Calculator::new();

    for character in input.chars() {
        match character {
            '+' => {
                if !calculator.current_input.is_empty() {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.current_input.clear();
                }
            },
            '-' => {
                if calculator.current_input.contains('-') || calculator.current_input.is_empty() {
                    calculator.current_input.push(character)
                } else {
                calculator.results.push(calculator.current_input.clone());
                calculator.current_input.clear();
                calculator.current_input.push(character);
                }
            },
            number => {
                if calculator.current_input.contains('-') {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.current_input.clear();
                    calculator.current_input.push(number);
                } else {
                    calculator.current_input.push(number);
                }
            },
        }
    }
    calculator.results.push(calculator.current_input);

    for entry in calculator.results {
        if entry.contains('-') {
            if entry.chars().count() % 2 == 1 {
                calculator.adds = match calculator.adds {
                    true => false,
                    false => true
                };
                continue;
            } else {
                continue;
            }
        }
        if calculator.adds {
            calculator.total += entry.parse::<i32>().unwrap();
        } else {
            calculator.total -= entry.parse::<i32>().unwrap();
            calculator.adds = true;
        }
    }
    calculator.total
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
    #[test]
    fn nine_plus_nine_minus_nine_minus_nine_is_zero() {
        assert_eq!(math("9+9-9-9"), 0);
    }
    #[test]
    fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
        assert_eq!(math("8  - 9     +9-----+++++"), 8);
    }
    #[test]
    #[should_panic]
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

最後我們增加兩個新方法。一個叫做 .clear(),清除 current_input()。另一個叫做 push_char(),把輸入推到 current_input() 上。這是我們重構後的代碼。

#![allow(unused)]
fn main() {
#[derive(Clone)]
struct Calculator {
    results: Vec<String>,
    current_input: String,
    total: i32,
    adds: bool,
}

impl Calculator {
    fn new() -> Self {
        Self {
            results: vec![],
            current_input: String::new(),
            total: 0,
            adds: true,
        }
    }

    fn clear(&mut self) {
        self.current_input.clear();
    }

    fn push_char(&mut self, character: char) {
        self.current_input.push(character);
    }
}

const OKAY_CHARACTERS: &str = "1234567890+- ";

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) ||
       !input.chars().take(2).any(|character| character.is_numeric()) {
        panic!("Please only input numbers, +-, or spaces");
    }

    let input = input.trim_end_matches(|x| "+- ".contains(x)).chars().filter(|x| *x != ' ').collect::<String>();
    let mut calculator = Calculator::new();

    for character in input.chars() {
        match character {
            '+' => {
                if !calculator.current_input.is_empty() {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.clear();
                }
            },
            '-' => {
                if calculator.current_input.contains('-') || calculator.current_input.is_empty() {
                    calculator.push_char(character)
                } else {
                calculator.results.push(calculator.current_input.clone());
                calculator.clear();
                calculator.push_char(character);
                }
            },
            number => {
                if calculator.current_input.contains('-') {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.clear();
                    calculator.push_char(number);
                } else {
                    calculator.push_char(number);
                }
            },
        }
    }
    calculator.results.push(calculator.current_input);

    for entry in calculator.results {
        if entry.contains('-') {
            if entry.chars().count() % 2 == 1 {
                calculator.adds = match calculator.adds {
                    true => false,
                    false => true
                };
                continue;
            } else {
                continue;
            }
        }
        if calculator.adds {
            calculator.total += entry.parse::<i32>().unwrap();
        } else {
            calculator.total -= entry.parse::<i32>().unwrap();
            calculator.adds = true;
        }
    }
    calculator.total
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
    #[test]
    fn nine_plus_nine_minus_nine_minus_nine_is_zero() {
        assert_eq!(math("9+9-9-9"), 0);
    }
    #[test]
    fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
        assert_eq!(math("8  - 9     +9-----+++++"), 8);
    }
    #[test]
    #[should_panic]
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

現在大概已經夠好了。我們可以寫更多的方法,但是像calculator.results.push(calculator.current_input.clone());這樣的行已經很清楚了。重構最好是在你完成後還能輕鬆閱讀代碼的時候。你不希望只是為了讓代碼變短而重構:例如,clc.clr()就比calculator.clear()差很多。

外部crate

外部crate的意思是 "別人的crate"。

在本節中,你差不多需要安裝Rust,但我們仍然可以只使用Playground。現在我們要學習如何導入別人寫的crate。這在Rust中很重要,原因有二。

  • 導入其他的crate很容易,並且...
  • Rust標準庫是相當小的。

這意味著,在Rust中,很多基本功能都需要用到外部Crate,這很正常。我們的想法是,如果使用外部Crate很方便,那麼你可以選擇最好的一個。也許一個人會為一個功能創建一個crate,然後其他人會創建一個更好的crate。

在本書中,我們只看最流行的crate,也就是每個使用Rust的人都知道的crate。

要開始學習外部Crate,我們將從最常見的Crate開始。rand.

rand

你有沒有注意到,我們還沒有使用任何隨機數?那是因為隨機數不在標準庫中。但是有很多crate "幾乎是標準庫",因為大家都在使用它們。在任何情況下,帶入一個 crate 是非常容易的。如果你的電腦上有Rust,有一個叫Cargo.toml的文件,裡面有這些信息。Cargo.toml文件在你啟動時是這樣的。

[package]
name = "rust_book"
version = "0.1.0"
authors = ["David MacLeod"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

現在,如果你想添加rand crate,在crates.io上搜索它,這是所有crate的去處。這將帶你到https://crates.io/crates/rand。當你點擊那個,你可以看到一個屏幕,上面寫著Cargo.toml rand = "0.7.3"。你所要做的就是在[dependencies]下添加這樣的內容:

[package]
name = "rust_book"
version = "0.1.0"
authors = ["David MacLeod"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.7.3"

然後Cargo會幫你完成剩下的工作。然後你就可以在rand文檔網站上開始編寫像本例代碼這樣的代碼。要想進入文檔,你可以點擊crates.io上的頁面中的docs按鈕。

關於Cargo的介紹就到這裡了:我們現在使用的還只是playground。幸運的是,playground已經安裝了前100個crate。所以你還不需要寫進Cargo.toml。在playground上,你可以想象,它有一個這樣的長長的列表,有100個crate。

[dependencies]
rand = "0.7.3"
some_other_crate = "0.1.0"
another_nice_crate = "1.7"

也就是說,如果要使用rand,你可以直接這樣做:

use rand; // This means the whole crate rand
          // On your computer you can't just write this;
          // you need to write in the Cargo.toml file first

fn main() {
    for _ in 0..5 {
        let random_u16 = rand::random::<u16>();
        print!("{} ", random_u16);
    }
}

每次都會打印不同的u16號碼,比如42266 52873 56528 46927 6867

rand中的主要功能是randomthread_rng(rng的意思是 "隨機數發生器")。而實際上如果你看random,它說:"這只是thread_rng().gen()的一個快捷方式"。所以其實是thread_rng基本做完了一切。

下面是一個簡單的例子,從1到10的數字。為了得到這些數字,我們在1到11之間使用.gen_range()

use rand::{thread_rng, Rng}; // Or just use rand::*; if we are lazy

fn main() {
    let mut number_maker = thread_rng();
    for _ in 0..5 {
        print!("{} ", number_maker.gen_range(1, 11));
    }
}

這將打印出7 2 4 8 6這樣的東西。

用隨機數我們可以做一些有趣的事情,比如為遊戲創建角色。我們將使用rand和其他一些我們知道的東西來創建它們。在這個遊戲中,我們的角色有六種狀態,用一個d6來表示他們。d6是一個立方體,當你投擲它時,它能給出1、2、3、4、5或6。每個角色都會擲三次d6,所以每個統計都在3到18之間。

但是有時候如果你的角色有一些低的東西,比如3或4,那就不公平了。比如說你的力量是3,你就不能拿東西。所以還有一種方法是用d6四次。你擲四次,然後扔掉最低的數字。所以如果你擲3,3,1,6,那麼你保留3,3,6=12。我們也會把這個方法做出來,所以遊戲的主人可以決定。

這是我們簡單的角色創建器。我們為數據統計創建了一個Character結構,甚至還實現了Display來按照我們想要的方式打印。

use rand::{thread_rng, Rng}; // Or just use rand::*; if we are lazy
use std::fmt; // Going to impl Display for our character


struct Character {
    strength: u8,
    dexterity: u8,    // This means "body quickness"
    constitution: u8, // This means "health"
    intelligence: u8,
    wisdom: u8,
    charisma: u8, // This means "popularity with people"
}

fn three_die_six() -> u8 { // A "die" is the thing you throw to get the number
    let mut generator = thread_rng(); // Create our random number generator
    let mut stat = 0; // This is the total
    for _ in 0..3 {
        stat += generator.gen_range(1..=6); // Add each time
    }
    stat // Return the total
}

fn four_die_six() -> u8 {
    let mut generator = thread_rng();
    let mut results = vec![]; // First put the numbers in a vec
    for _ in 0..4 {
        results.push(generator.gen_range(1..=6));
    }
    results.sort(); // Now a result like [4, 3, 2, 6] becomes [2, 3, 4, 6]
    results.remove(0); // Now it would be [3, 4, 6]
    results.iter().sum() // Return this result
}

enum Dice {
    Three,
    Four
}

impl Character {
    fn new(dice: Dice) -> Self { // true for three dice, false for four
        match dice {
            Dice::Three => Self {
                strength: three_die_six(),
                dexterity: three_die_six(),
                constitution: three_die_six(),
                intelligence: three_die_six(),
                wisdom: three_die_six(),
                charisma: three_die_six(),
            },
            Dice::Four => Self {
                strength: four_die_six(),
                dexterity: four_die_six(),
                constitution: four_die_six(),
                intelligence: four_die_six(),
                wisdom: four_die_six(),
                charisma: four_die_six(),
            },
        }
    }
    fn display(&self) { // We can do this because we implemented Display below
        println!("{}", self);
        println!();
    }
}

impl fmt::Display for Character { // Just follow the code for in https://doc.rust-lang.org/std/fmt/trait.Display.html and change it a bit
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "Your character has these stats:
strength: {}
dexterity: {}
constitution: {}
intelligence: {}
wisdom: {}
charisma: {}",
            self.strength,
            self.dexterity,
            self.constitution,
            self.intelligence,
            self.wisdom,
            self.charisma
        )
    }
}



fn main() {
    let weak_billy = Character::new(Dice::Three);
    let strong_billy = Character::new(Dice::Four);
    weak_billy.display();
    strong_billy.display();
}

它會打印出這樣的東西。

#![allow(unused)]
fn main() {
Your character has these stats:
strength: 9
dexterity: 15
constitution: 15
intelligence: 8
wisdom: 11
charisma: 9

Your character has these stats:
strength: 9
dexterity: 13
constitution: 14
intelligence: 16
wisdom: 16
charisma: 10
}

有四個骰子的角色通常在大多數事情上都會好一點。

rayon

rayon 是一個流行的crate,它可以讓你加快 Rust 代碼的速度。它之所以受歡迎,是因為它無需像 thread::spawn 這樣的東西就能創建線程。換句話說,它之所以受歡迎是因為它既有效又容易編寫。比如說

  • .iter(), .iter_mut(), into_iter()在rayon中是這樣寫的:
  • .par_iter(), .par_iter_mut(), par_into_iter(). 所以你只要加上par_,你的代碼就會變得快很多。(par的意思是 "並行")

其他方法也一樣:.chars()就是.par_chars(),以此類推。

這裡舉個例子,一段簡單的代碼,卻讓計算機做了很多工作。

fn main() {
    let mut my_vec = vec![0; 200_000];
    my_vec.iter_mut().enumerate().for_each(|(index, number)| *number+=index+1);
    println!("{:?}", &my_vec[5000..5005]);
}

它創建了一個有20萬項的向量:每一項都是0,然後調用.enumerate()來獲取每個數字的索引,並將0改為索引號。它的打印時間太長,所以我們只打印5000到5004項。這在Rust中還是非常快的,但如果你願意,你可以用Rayon讓它更快。代碼幾乎是一樣的。

use rayon::prelude::*; // Import rayon

fn main() {
    let mut my_vec = vec![0; 200_000];
    my_vec.par_iter_mut().enumerate().for_each(|(index, number)| *number+=index+1); // add par_ to iter_mut
    println!("{:?}", &my_vec[5000..5005]);
}

就這樣了。rayon還有很多其他的方法來定製你想做的事情,但最簡單的就是 "添加_par,讓你的程序更快"。

serde

serde是一個流行的crate,它可以在JSON、YAML等格式間相互轉換。最常見的使用方法是通過創建一個struct,上面有兩個屬性。它看起來是這樣的

#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize, Debug)]
struct Point {
    x: i32,
    y: i32,
}
}

SerializeDeserializetrait是使轉換變得簡單的原因。(這也是serde這個名字的由來)如果你的結構體上有這兩個trait,那麼你只需要調用一個方法就可以把它轉化為JSON或其他任何東西。

regex

regex crate 可以讓你使用 正則表達式 搜索文本。有了它,你可以通過一次搜索得到諸如 colour, color, colourscolors 的匹配信息。正則表達式是另一門語言,如果你想使用它們,也必須學會。

chrono

chrono是為那些需要更多時間功能的人準備的主要crate。我們現在來看一下標準庫,它有時間的功能,但是如果你需要更多的功能,那麼這個crate是一個不錯的選擇。

標準庫之旅

現在你已經知道了很多Rust的知識,你將能夠理解標準庫裡面的大部分東西。它裡面的代碼已經不是那麼可怕了。讓我們來看看它裡面一些我們還沒有學過的部分。本篇遊記將介紹標準庫的大部分部分,你不需要安裝Rust。我們將重溫很多我們已經知道的內容,這樣我們就可以更深入地學習它們。

數組

關於數組需要注意的一點是,它們沒有實現Iterator.。這意味著,如果你有一個數組,你不能使用for。但是你可以對它們使用 .iter() 這樣的方法。或者你可以使用&來得到一個切片。實際上,如果你嘗試使用for,編譯器會準確地告訴你。

fn main() {
    // ⚠️
    let my_cities = ["Beirut", "Tel Aviv", "Nicosia"];

    for city in my_cities {
        println!("{}", city);
    }
}

消息是:

error[E0277]: `[&str; 3]` is not an iterator
 --> src\main.rs:5:17
  |
  |                 ^^^^^^^^^ borrow the array with `&` or call `.iter()` on it to iterate over it

所以讓我們試試這兩種方法。它們的結果是一樣的。

fn main() {
    let my_cities = ["Beirut", "Tel Aviv", "Nicosia"];

    for city in &my_cities {
        println!("{}", city);
    }
    for city in my_cities.iter() {
        println!("{}", city);
    }
}

這個打印:

Beirut
Tel Aviv
Nicosia
Beirut
Tel Aviv
Nicosia

如果你想從一個數組中獲取變量,你可以把它們的名字放在 [] 中來解構它。這與在 match 語句中使用元組或從結構體中獲取變量是一樣的。

fn main() {
    let my_cities = ["Beirut", "Tel Aviv", "Nicosia"];
    let [city1, city2, city3] = my_cities;
    println!("{}", city1);
}

打印出Beirut.

char

您可以使用.escape_unicode()的方法來獲取char的Unicode號碼。

fn main() {
    let korean_word = "청춘예찬";
    for character in korean_word.chars() {
        print!("{} ", character.escape_unicode());
    }
}

這將打印出 u{ccad} u{cd98} u{c608} u{cc2c}

你可以使用 From trait從 u8 中得到一個字符,但對於 u32,你使用 TryFrom,因為它可能無法工作。u32中的數字比Unicode中的字符多很多。我們可以通過一個簡單的演示來瞭解。

use std::convert::TryFrom; // You need to bring TryFrom in to use it
use rand::prelude::*;      // We will use random numbers too

fn main() {
    let some_character = char::from(99); // This one is easy - no need for TryFrom
    println!("{}", some_character);

    let mut random_generator = rand::thread_rng();
    // This will try 40,000 times to make a char from a u32.
    // The range is 0 (std::u32::MIN) to u32's highest number (std::u32::MAX). If it doesn't work, we will give it '-'.
    for _ in 0..40_000 {
        let bigger_character = char::try_from(random_generator.gen_range(std::u32::MIN..std::u32::MAX)).unwrap_or('-');
        print!("{}", bigger_character)
    }
}

幾乎每次都會生成一個-。這是你會看到的那種輸出的一部分。

------------------------------------------------------------------------𤒰---------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-------------------------------------------------------------춗--------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
------------򇍜----------------------------------------------------

所以,你要用TryFrom是件好事。

另外,從2020年8月底開始,你現在可以從char中得到一個String。(String實現了From<char>)只要寫String::from(),然後在裡面放一個char

整數

這些類型的數學方法有很多,另外還有一些其他的方法。下面是一些最有用的。

.checked_add(), .checked_sub(), .checked_mul(), .checked_div(). 如果你認為你可能會得到一個不適合類型的數字,這些都是不錯的方法。它們會返回一個 Option,這樣你就可以安全地檢查你的數學計算是否正常,而不會讓程序崩潰。

fn main() {
    let some_number = 200_u8;
    let other_number = 200_u8;

    println!("{:?}", some_number.checked_add(other_number));
    println!("{:?}", some_number.checked_add(1));
}

這個打印:

None
Some(201)

你會注意到,在整數的頁面上,經常說rhs。這意味著 "右邊",也就是你做一些數學運算時的右操作數。比如在5 + 6中,5在左邊,6在右邊,所以6就是rhs。這個不是關鍵詞,但是你會經常看到,所以知道就好。

說到這裡,我們來學習一下如何實現Add。在你實現了Add之後,你可以在你創建的類型上使用+。你需要自己實現Add,因為add可以表達很多意思。這是標準庫頁面中的例子。

#![allow(unused)]
fn main() {
use std::ops::Add; // first bring in Add

#[derive(Debug, Copy, Clone, PartialEq)] // PartialEq is probably the most important part here. You want to be able to compare numbers
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Self; // Remember, this is called an "associated type": a "type that goes together".
                        // In this case it's just another Point

    fn add(self, other: Self) -> Self {
        Self {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}
}

現在讓我們為自己的類型實現Add。讓我們想象一下,我們想把兩個國家加在一起,這樣我們就可以比較它們的經濟。它看起來像這樣:

use std::fmt;
use std::ops::Add;

#[derive(Clone)]
struct Country {
    name: String,
    population: u32,
    gdp: u32, // This is the size of the economy
}

impl Country {
    fn new(name: &str, population: u32, gdp: u32) -> Self {
        Self {
            name: name.to_string(),
            population,
            gdp,
        }
    }
}

impl Add for Country {
    type Output = Self;

    fn add(self, other: Self) -> Self {
        Self {
            name: format!("{} and {}", self.name, other.name), // We will add the names together,
            population: self.population + other.population, // and the population,
            gdp: self.gdp + other.gdp,   // and the GDP
        }
    }
}

impl fmt::Display for Country {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "In {} are {} people and a GDP of ${}", // Then we can print them all with just {}
            self.name, self.population, self.gdp
        )
    }
}

fn main() {
    let nauru = Country::new("Nauru", 10_670, 160_000_000);
    let vanuatu = Country::new("Vanuatu", 307_815, 820_000_000);
    let micronesia = Country::new("Micronesia", 104_468, 367_000_000);

    // We could have given Country a &str instead of a String for the name. But we would have to write lifetimes everywhere
    // and that would be too much for a small example. Better to just clone them when we call println!.
    println!("{}", nauru.clone());
    println!("{}", nauru.clone() + vanuatu.clone());
    println!("{}", nauru + vanuatu + micronesia);
}

這個打印:

In Nauru are 10670 people and a GDP of $160000000
In Nauru and Vanuatu are 318485 people and a GDP of $980000000
In Nauru and Vanuatu and Micronesia are 422953 people and a GDP of $1347000000

以後在這段代碼中,我們可以把.fmt()改成更容易閱讀的數字顯示。

另外三個叫SubMulDiv,實現起來基本一樣。+=-=*=/=,只要加上Assign:AddAssignSubAssignMulAssignDivAssign即可。你可以看到完整的列表這裡,因為還有很多。例如 % 被稱為 Rem, - 被稱為 Neg, 等等。

浮點數

f32f64有非常多的方法,你在做數學計算的時候會用到。我們不看這些,但這裡有一些你可能會用到的方法。它們分別是 .floor(), .ceil(), .round(), 和 .trunc(). 所有這些方法都返回一個 f32f64,它像一個整數,小數點後面是 0。它們是這樣做的。

  • .floor(): 給你下一個最低的整數.
  • .ceil(): 給你下一個最高的整數。
  • .round(): 如果小數部分大於等於0.5,返回數值加1;如果小數部分小於0.5,返回相同數值。這就是所謂的四捨五入,因為它給你一個 "舍入"的數字(一個數字的簡短形式)。
  • .trunc():只是把小數點號後的部分截掉。Truncate是 "截斷"的意思。

這裡有一個簡單的函數來打印它們。

fn four_operations(input: f64) {
    println!(
"For the number {}:
floor: {}
ceiling: {}
rounded: {}
truncated: {}\n",
        input,
        input.floor(),
        input.ceil(),
        input.round(),
        input.trunc()
    );
}

fn main() {
    four_operations(9.1);
    four_operations(100.7);
    four_operations(-1.1);
    four_operations(-19.9);
}

這個打印:

For the number 9.1:
floor: 9
ceiling: 10
rounded: 9 // because less than 9.5
truncated: 9

For the number 100.7:
floor: 100
ceiling: 101
rounded: 101 // because more than 100.5
truncated: 100

For the number -1.1:
floor: -2
ceiling: -1
rounded: -1
truncated: -1

For the number -19.9:
floor: -20
ceiling: -19
rounded: -20
truncated: -19

f32f64 有一個叫做 .max().min() 的方法,可以得到兩個數字中較大或較小的數字。(對於其他類型,你可以直接使用std::cmp::maxstd::cmp::min。)下面是用.fold()來得到最高或最低數的方法。你又可以看到,.fold()不僅僅是用來加數字的。

fn main() {
    let my_vec = vec![8.0_f64, 7.6, 9.4, 10.0, 22.0, 77.345, 10.22, 3.2, -7.77, -10.0];
    let maximum = my_vec.iter().fold(f64::MIN, |current_number, next_number| current_number.max(*next_number)); // Note: start with the lowest possible number for an f64.
    let minimum = my_vec.iter().fold(f64::MAX, |current_number, next_number| current_number.min(*next_number)); // And here start with the highest possible number
    println!("{}, {}", maximum, minimum);
}

bool

在 Rust 中,如果你願意,你可以把 bool 變成一個整數,因為這樣做是安全的。但你不能反過來做。如你所見,true變成了1,false變成了0。

fn main() {
    let true_false = (true, false);
    println!("{} {}", true_false.0 as u8, true_false.1 as i32);
}

這將打印出1 0。如果你告訴編譯器類型,也可以使用 .into()

fn main() {
    let true_false: (i128, u16) = (true.into(), false.into());
    println!("{} {}", true_false.0, true_false.1);
}

這打印的是一樣的東西。

從Rust 1.50(2021年2月發佈)開始,有一個叫做 then()的方法,它將一個 bool變成一個 Option。使用then()時需要一個閉包,如果item是true,閉包就會被調用。同時,無論從閉包中返回什麼,都會進入Option中。下面是一個小例子:

fn main() {

    let (tru, fals) = (true.then(|| 8), false.then(|| 8));
    println!("{:?}, {:?}", tru, fals);
}

這個打印 Some(8), None

下面是一個較長的例子:

fn main() {
    let bool_vec = vec![true, false, true, false, false];

    let option_vec = bool_vec
        .iter()
        .map(|item| {
            item.then(|| { // Put this inside of map so we can pass it on
                println!("Got a {}!", item);
                "It's true, you know" // This goes inside Some if it's true
                                      // Otherwise it just passes on None
            })
        })
        .collect::<Vec<_>>();

    println!("Now we have: {:?}", option_vec);

    // That printed out the Nones too. Let's filter map them out in a new Vec.
    let filtered_vec = option_vec.into_iter().filter_map(|c| c).collect::<Vec<_>>();

    println!("And without the Nones: {:?}", filtered_vec);
}

將打印:

Got a true!
Got a true!
Now we have: [Some("It\'s true, you know"), None, Some("It\'s true, you know"), None, None]
And without the Nones: ["It\'s true, you know", "It\'s true, you know"]

Vec

Vec有很多方法我們還沒有看。先說說.sort().sort()一點都不奇怪。它使用&mut self來對一個向量進行排序。

fn main() {
    let mut my_vec = vec![100, 90, 80, 0, 0, 0, 0, 0];
    my_vec.sort();
    println!("{:?}", my_vec);
}

這樣打印出來的是[0, 0, 0, 0, 0, 80, 90, 100]。但還有一種更有趣的排序方式叫.sort_unstable(),它通常更快。它之所以更快,是因為它不在乎排序前後相同數字的先後順序。在常規的.sort()中,你知道最後的0, 0, 0, 0, 0會在.sort()之後的順序相同。但是.sort_unstable()可能會把最後一個0移到索引0,然後把第三個最後的0移到索引2,等等。

.dedup()的意思是 "去重複"。它將刪除一個向量中相同的元素,但只有當它們彼此相鄰時才會刪除。接下來這段代碼不會只打印"sun", "moon"

fn main() {
    let mut my_vec = vec!["sun", "sun", "moon", "moon", "sun", "moon", "moon"];
    my_vec.dedup();
    println!("{:?}", my_vec);
}

它只是把另一個 "sun"旁邊的 "sun"去掉,然後把一個 "moon"旁邊的 "moon"去掉,再把另一個 "moon"旁邊的 "moon"去掉。結果是 ["sun", "moon", "sun", "moon"].

如果你想把每個重複的東西都去掉,就先.sort():

fn main() {
    let mut my_vec = vec!["sun", "sun", "moon", "moon", "sun", "moon", "moon"];
    my_vec.sort();
    my_vec.dedup();
    println!("{:?}", my_vec);
}

結果:["moon", "sun"].

String

你會記得,String有點像Vec。它很像Vec,你可以調用很多相同的方法。比如說,你可以用String::with_capacity()創建一個,如果你需要多次用.push()推一個char,或者用.push_str()推一個&str。下面是一個有多次內存分配的String的例子。

fn main() {
    let mut push_string = String::new();
    let mut capacity_counter = 0; // capacity starts at 0
    for _ in 0..100_000 { // Do this 100,000 times
        if push_string.capacity() != capacity_counter { // First check if capacity is different now
            println!("{}", push_string.capacity()); // If it is, print it
            capacity_counter = push_string.capacity(); // then update the counter
        }
        push_string.push_str("I'm getting pushed into the string!"); // and push this in every time
    }
}

這個打印:

35
70
140
280
560
1120
2240
4480
8960
17920
35840
71680
143360
286720
573440
1146880
2293760
4587520

我們不得不重新分配(把所有東西複製過來)18次。但既然我們知道了最終的容量,我們可以馬上設置容量,不需要重新分配:只設置一次String容量就夠了。

fn main() {
    let mut push_string = String::with_capacity(4587520); // We know the exact number. Some different big number could work too
    let mut capacity_counter = 0;
    for _ in 0..100_000 {
        if push_string.capacity() != capacity_counter {
            println!("{}", push_string.capacity());
            capacity_counter = push_string.capacity();
        }
        push_string.push_str("I'm getting pushed into the string!");
    }
}

而這個打印4587520。完美的! 我們再也不用分配了。

當然,實際長度肯定比這個小。如果你試了100001次,101000次等等,還是會說4587520。這是因為每次的容量都是之前的2倍。不過我們可以用.shrink_to_fit()來縮小它(和Vec一樣)。我們的String已經非常大了,我們不想再給它增加任何東西,所以我們可以把它縮小一點。但是隻有在你有把握的情況下才可以這樣做:下面是原因。

fn main() {
    let mut push_string = String::with_capacity(4587520);
    let mut capacity_counter = 0;
    for _ in 0..100_000 {
        if push_string.capacity() != capacity_counter {
            println!("{}", push_string.capacity());
            capacity_counter = push_string.capacity();
        }
        push_string.push_str("I'm getting pushed into the string!");
    }
    push_string.shrink_to_fit();
    println!("{}", push_string.capacity());
    push_string.push('a');
    println!("{}", push_string.capacity());
    push_string.shrink_to_fit();
    println!("{}", push_string.capacity());
}

這個打印:

4587520
3500000
7000000
3500001

所以首先我們的大小是4587520,但我們沒有全部使用。我們用了.shrink_to_fit(),然後把大小降到了3500000。但是我們忘記了我們需要推上一個 a。當我們這樣做的時候,Rust 看到我們需要更多的空間,給了我們雙倍的空間:現在是 7000000。Whoops! 所以我們又調用了.shrink_to_fit(),現在又回到了3500001

.pop()String有用,就像對Vec一樣。

fn main() {
    let mut my_string = String::from(".daer ot drah tib elttil a si gnirts sihT");
    loop {
        let pop_result = my_string.pop();
        match pop_result {
            Some(character) => print!("{}", character),
            None => break,
        }
    }
}

這打印的是This string is a little bit hard to read.,因為它是從最後一個字符開始的。

.retain()是一個使用閉包的方法,這對String來說是罕見的。就像在迭代器上的.filter()一樣。

fn main() {
    let mut my_string = String::from("Age: 20 Height: 194 Weight: 80");
    my_string.retain(|character| character.is_alphabetic() || character == ' '); // Keep if a letter or a space
    dbg!(my_string); // Let's use dbg!() for fun this time instead of println!
}

這個打印:

[src\main.rs:4] my_string = "Age  Height  Weight "

OsString和CString

std::ffistd的一部分,它幫助你將Rust與其他語言或操作系統一起使用。它有OsStringCString這樣的類型,它們就像操作系統的String或語言C的String一樣,它們各自也有自己的&str類型:OsStrCStrffi的意思是 "foreign function interface"(外部函數接口)。

當你必須與一個沒有Unicode的操作系統一起工作時,你可以使用OsString。所有的Rust字符串都是unicode,但不是每個操作系統支持。下面是標準庫中關於為什麼我們有OsString的簡單英文解釋。

  • Unix系統(Linux等)上的字符串可能是很多沒有0的字節組合在一起。而且有時你會把它們讀成Unicode UTF-8。
  • Windows上的字符串可能是由隨機的16位值組成的,沒有0。有時你會把它們讀成Unicode UTF-16。
  • 在Rust中,字符串總是有效的UTF-8,其中可能包含0。

所以,OsString被設計為支持它們讀取。

你可以用一個OsString做所有常規的事情,比如OsString::from("Write something here")。它還有一個有趣的方法,叫做 .into_string(),試圖把自己變成一個常規的 String。它返回一個 Result,但 Err 部分只是原來的 OsString

#![allow(unused)]
fn main() {
// 🚧
pub fn into_string(self) -> Result<String, OsString>
}

所以如果不行的話,那你就把它找回來。你不能調用.unwrap(),因為它會崩潰,但是你可以使用match來找回OsString。我們通過調用不存在的方法來測試一下。

use std::ffi::OsString;

fn main() {
    // ⚠️
    let os_string = OsString::from("This string works for your OS too.");
    match os_string.into_string() {
        Ok(valid) => valid.thth(),           // Compiler: "What's .thth()??"
        Err(not_valid) => not_valid.occg(),  // Compiler: "What's .occg()??"
    }
}

然後編譯器準確地告訴我們我們想知道的東西。

error[E0599]: no method named `thth` found for struct `std::string::String` in the current scope
 --> src/main.rs:6:28
  |
6 |         Ok(valid) => valid.thth(),
  |                            ^^^^ method not found in `std::string::String`

error[E0599]: no method named `occg` found for struct `std::ffi::OsString` in the current scope
 --> src/main.rs:7:37
  |
7 |         Err(not_valid) => not_valid.occg(),
  |                                     ^^^^ method not found in `std::ffi::OsString`

我們可以看到,valid的類型是Stringnot_valid的類型是OsString

Mem

std::mem有一些非常有趣的方法。我們已經看到了一些,比如.size_of().size_of_val().drop()

use std::mem;

fn main() {
    println!("{}", mem::size_of::<i32>());
    let my_array = [8; 50];
    println!("{}", mem::size_of_val(&my_array));
    let mut some_string = String::from("You can drop a String because it's on the heap");
    mem::drop(some_string);
    // some_string.clear();   If we did this it would panic
}

這個打印:

4
200

下面是mem中的一些其他方法。

swap(): 用這個方法你可以交換兩個變量之間的值。你可以通過為每個變量創建一個可變引用來做。當你有兩個東西想交換,而Rust因為借用規則不讓你交換時,這很有幫助。或者只是當你想快速切換兩個東西的時候。

這裡有一個例子。

use std::{mem, fmt};

struct Ring { // Create a ring from Lord of the Rings
    owner: String,
    former_owner: String,
    seeker: String, // seeker means "person looking for it"
}

impl Ring {
    fn new(owner: &str, former_owner: &str, seeker: &str) -> Self {
        Self {
            owner: owner.to_string(),
            former_owner: former_owner.to_string(),
            seeker: seeker.to_string(),
        }
    }
}

impl fmt::Display for Ring { // Display to show who has it and who wants it
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "{} has the ring, {} used to have it, and {} wants it", self.owner, self.former_owner, self.seeker)
        }
}

fn main() {
    let mut one_ring = Ring::new("Frodo", "Gollum", "Sauron");
    println!("{}", one_ring);
    mem::swap(&mut one_ring.owner, &mut one_ring.former_owner); // Gollum got the ring back for a second
    println!("{}", one_ring);
}

這將打印:

Frodo has the ring, Gollum used to have it, and Sauron wants it
Gollum has the ring, Frodo used to have it, and Sauron wants it

replace():這個就像swap一樣,其實裡面也用了swap,你可以看到。

#![allow(unused)]
fn main() {
pub fn replace<T>(dest: &mut T, mut src: T) -> T {
    swap(dest, &mut src);
    src
}
}

所以它只是做了一個交換,然後返回另一個元素。有了這個,你就用你放進去的其他東西來替換這個值。因為它返回的是舊的值,所以你應該用let來使用它。下面是一個簡單的例子。

use std::mem;

struct City {
    name: String,
}

impl City {
    fn change_name(&mut self, name: &str) {
        let old_name = mem::replace(&mut self.name, name.to_string());
        println!(
            "The city once called {} is now called {}.",
            old_name, self.name
        );
    }
}

fn main() {
    let mut capital_city = City {
        name: "Constantinople".to_string(),
    };
    capital_city.change_name("Istanbul");
}

這樣就會打印出The city once called Constantinople is now called Istanbul.

有一個函數叫.take(),和.replace()一樣,但它在元素中留下了默認值。 你會記得,默認值通常是0、""之類的東西。這裡是簽名。

#![allow(unused)]
fn main() {
// 🚧
pub fn take<T>(dest: &mut T) -> T
where
    T: Default,
}

所以你可以做這樣的事情。

use std::mem;

fn main() {
    let mut number_vec = vec![8, 7, 0, 2, 49, 9999];
    let mut new_vec = vec![];

    number_vec.iter_mut().for_each(|number| {
        let taker = mem::take(number);
        new_vec.push(taker);
    });

    println!("{:?}\n{:?}", number_vec, new_vec);
}

你可以看到,它將所有數字都替換為0:沒有刪除任何索引。

[0, 0, 0, 0, 0, 0]
[8, 7, 0, 2, 49, 9999]

當然,對於你自己的類型,你可以把Default實現成任何你想要的類型。我們來看一個例子,我們有一個Bank和一個Robber。每次他搶了Bank,他就會在桌子上拿到錢。但是辦公桌可以隨時從後面拿錢,所以它永遠有50。我們將為此自制一個類型,所以它將永遠有50。下面是它的工作原理。

use std::mem;
use std::ops::{Deref, DerefMut}; // We will use this to get the power of u32

struct Bank {
    money_inside: u32,
    money_at_desk: DeskMoney, // This is our "smart pointer" type. It has its own default, but it will use u32
}

struct DeskMoney(u32);

impl Default for DeskMoney {
    fn default() -> Self {
        Self(50) // default is always 50, not 0
    }
}

impl Deref for DeskMoney { // With this we can access the u32 using *
    type Target = u32;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DerefMut for DeskMoney { // And with this we can add, subtract, etc.
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl Bank {
    fn check_money(&self) {
        println!(
            "There is ${} in the back and ${} at the desk.\n",
            self.money_inside, *self.money_at_desk // Use * so we can just print the u32
        );
    }
}

struct Robber {
    money_in_pocket: u32,
}

impl Robber {
    fn check_money(&self) {
        println!("The robber has ${} right now.\n", self.money_in_pocket);
    }

    fn rob_bank(&mut self, bank: &mut Bank) {
        let new_money = mem::take(&mut bank.money_at_desk); // Here it takes the money, and leaves 50 because that is the default
        self.money_in_pocket += *new_money; // Use * because we can only add u32. DeskMoney can't add
        bank.money_inside -= *new_money;    // Same here
        println!("She robbed the bank. She now has ${}!\n", self.money_in_pocket);
    }
}

fn main() {
    let mut bank_of_klezkavania = Bank { // Set up our bank
        money_inside: 5000,
        money_at_desk: DeskMoney(50),
    };
    bank_of_klezkavania.check_money();

    let mut robber = Robber { // Set up our robber
        money_in_pocket: 50,
    };
    robber.check_money();

    robber.rob_bank(&mut bank_of_klezkavania); // Rob, then check money
    robber.check_money();
    bank_of_klezkavania.check_money();

    robber.rob_bank(&mut bank_of_klezkavania); // Do it again
    robber.check_money();
    bank_of_klezkavania.check_money();

}

這將打印:

There is $5000 in the back and $50 at the desk.

The robber has $50 right now.

She robbed the bank. She now has $100!

The robber has $100 right now.

There is $4950 in the back and $50 at the desk.

She robbed the bank. She now has $150!

The robber has $150 right now.

There is $4900 in the back and $50 at the desk.

你可以看到桌子上總是有50美元。

Prelude

標準庫也有一個prelude,這就是為什麼你不用寫use std::vec::Vec這樣的東西來創建一個Vec。你可以在這裡看到所有這些元素,並且大致瞭解:

  • std::marker::{Copy, Send, Sized, Sync, Unpin}. 你以前沒有見過Unpin,因為幾乎每一種類型都會用到它(比如Sized,也很常見)。"Pin"的意思是不讓東西動。在這種情況下,Pin意味著它在內存中不能移動,但大多數元素都有Unpin,所以你可以移動。這就是為什麼像std::mem::replace這樣的函數能用,因為它們沒有被釘住。
  • std::ops::{Drop, Fn, FnMut, FnOnce}.
  • std::mem::drop
  • std::boxed::Box.
  • std::borrow::ToOwned. 你之前用Cow看到過一點,它可以把借來的內容變成自己的。它使用.to_owned()來實現這個功能。你也可以在&str上使用.to_owned(),得到一個String,對於其他借來的值也是一樣。
  • std::clone::Clone
  • std::cmp::{PartialEq, PartialOrd, Eq, Ord}.
  • std::convert::{AsRef, AsMut, Into, From}.
  • std::default::Default.
  • std::iter::{Iterator, Extend, IntoIterator, DoubleEndedIterator, ExactSizeIterator}. 我們之前用.rev()來做迭代器:這實際上是做了一個DoubleEndedIteratorExactSizeIterator只是類似於0..10的東西:它已經知道自己的.len()是10。其他迭代器不知道它們的長度是肯定的。
  • std::option::Option::{self, Some, None}.
  • std::result::Result::{self, Ok, Err}.
  • std::string::{String, ToString}.
  • std::vec::Vec.

如果你因為某些原因不想要這個prelude怎麼辦?就加屬性#![no_implicit_prelude]。我們來試一試,看編譯器的抱怨。

// ⚠️
#![no_implicit_prelude]
fn main() {
    let my_vec = vec![8, 9, 10];
    let my_string = String::from("This won't work");
    println!("{:?}, {}", my_vec, my_string);
}

現在Rust根本不知道你想做什麼。

error: cannot find macro `println` in this scope
 --> src/main.rs:5:5
  |
5 |     println!("{:?}, {}", my_vec, my_string);
  |     ^^^^^^^

error: cannot find macro `vec` in this scope
 --> src/main.rs:3:18
  |
3 |     let my_vec = vec![8, 9, 10];
  |                  ^^^

error[E0433]: failed to resolve: use of undeclared type or module `String`
 --> src/main.rs:4:21
  |
4 |     let my_string = String::from("This won't work");
  |                     ^^^^^^ use of undeclared type or module `String`

error: aborting due to 3 previous errors

因此,對於這個簡單的代碼,你需要告訴Rust使用extern(外部)crate,叫做std,然後是你想要的元素。這裡是我們要做的一切,只是為了創建一個Vec和一個String,並打印它。

#![no_implicit_prelude]

extern crate std; // Now you have to tell Rust that you want to use a crate called std
use std::vec; // We need the vec macro
use std::string::String; // and string
use std::convert::From; // and this to convert from a &str to the String
use std::println; // and this to print

fn main() {
    let my_vec = vec![8, 9, 10];
    let my_string = String::from("This won't work");
    println!("{:?}, {}", my_vec, my_string);
}

現在終於成功了,打印出[8, 9, 10], This won't work。所以你可以明白為什麼Rust要用prelude了。但如果你願意,你不需要使用它。而且你甚至可以使用#![no_std](我們曾經看到過),用於你連堆棧內存這種東西都用不上的時候。但大多數時候,你根本不用考慮不用prelude或std

那麼為什麼之前我們沒有看到extern這個關鍵字呢?是因為你已經不需要它了。以前,當帶入外部crate時,你必須使用它。所以以前要使用rand,你必須要寫成:

#![allow(unused)]
fn main() {
extern crate rand;
}

然後用 use 語句來表示你想使用的修改、trait等。但現在Rust編譯器已經不需要這些幫助了--你只需要使用use,rust就知道在哪裡可以找到它。所以你幾乎再也不需要extern crate了,但在其他人的Rust代碼中,你可能仍然會在頂部看到它。

Time

std::time是你可以找到時間函數的地方。(如果你想要更多的功能,chrono這樣的crate也可以。)最簡單的功能就是用Instant::now()獲取系統時間即可。

use std::time::Instant;

fn main() {
    let time = Instant::now();
    println!("{:?}", time);
}

如果你打印出來,你會得到這樣的東西。Instant { tv_sec: 2738771, tv_nsec: 685628140 }. 這說的是秒和納秒,但用處不大。比如你看2738771秒(寫於8月),就是31.70天。這和月份、日子沒有任何關係。但是Instant的頁面告訴我們,它本身不應該有用。它說它是 "不透明的,只有和Duration一起才有用"。Opaque的意思是 "你搞不清楚",而Duration的意思是 "過了多少時間"。所以它只有在做比較時間這樣的事情時才有用。

如果你看左邊的trait,其中一個是Sub<Instant>。也就是說我們可以用-來減去一個。而當我們點擊[src]看它的作用時,頁面顯示:

#![allow(unused)]
fn main() {
impl Sub<Instant> for Instant {
    type Output = Duration;

    fn sub(self, other: Instant) -> Duration {
        self.duration_since(other)
    }
}
}

因此,它需要一個Instant,並使用.duration_since()給出一個Duration。讓我們試著打印一下。我們將創建兩個相鄰的 Instant::now(),然後讓程序忙活一會兒,再創建一個 Instant::now()。然後我們再創建一個Instant::now(). 最後,我們來看看用了多長時間。

use std::time::Instant;

fn main() {
    let time1 = Instant::now();
    let time2 = Instant::now(); // These two are right next to each other

    let mut new_string = String::new();
    loop {
        new_string.push('წ'); // Make Rust push this Georgian letter onto the String
        if new_string.len() > 100_000 { //  until it is 100,000 bytes long
            break;
        }
    }
    let time3 = Instant::now();
    println!("{:?}", time2 - time1);
    println!("{:?}", time3 - time1);
}

這將打印出這樣的東西。

1.025µs
683.378µs

所以,這只是1微秒多與683毫秒。我們可以看到,Rust確實花了一些時間來做。

不過我們可以用一個Instant做一件有趣的事情。 我們可以把它變成Stringformat!("{:?}", Instant::now());。它的樣子是這樣的:

use std::time::Instant;

fn main() {
    let time1 = format!("{:?}", Instant::now());
    println!("{}", time1);
}

這樣就會打印出類似Instant { tv_sec: 2740773, tv_nsec: 632821036 }的東西。這是沒有用的,但是如果我們使用 .iter().rev() 以及 .skip(2),我們可以跳過最後的 } 。我們可以用它來創建一個隨機數發生器。

use std::time::Instant;

fn bad_random_number(digits: usize) {
    if digits > 9 {
        panic!("Random number can only be up to 9 digits");
    }
    let now = Instant::now();
    let output = format!("{:?}", now);

    output
        .chars()
        .rev()
        .skip(2)
        .take(digits)
        .for_each(|character| print!("{}", character));
    println!();
}

fn main() {
    bad_random_number(1);
    bad_random_number(1);
    bad_random_number(3);
    bad_random_number(3);
}

這樣就會打印出類似這樣的內容:

6
4
967
180

這個函數被稱為bad_random_number,因為它不是一個很好的隨機數生成器。Rust有更好的crate,可以用比rand更少的代碼創建隨機數,比如fastrand。但這是一個很好的例子,你可以利用你的想象力用Instant來做一些事情。

當你有一個線程時,你可以使用std::thread::sleep使它停止一段時間。當你這樣做時,你必須給它一個duration。你不必創建多個線程來做這件事,因為每個程序至少在一個線程上。sleep雖然需要一個Duration,所以它可以知道要睡多久。你可以這樣選單位:Duration::from_millis(), Duration::from_secs, 等等。這裡舉一個例子:

use std::time::Duration;
use std::thread::sleep;

fn main() {
    let three_seconds = Duration::from_secs(3);
    println!("I must sleep now.");
    sleep(three_seconds);
    println!("Did I miss anything?");
}

這將只打印

I must sleep now.
Did I miss anything?

但線程在三秒鐘內什麼也不做。當你有很多線程需要經常嘗試一些事情時,比如連接,你通常會使用.sleep()。你不希望線程在一秒鐘內使用你的處理器嘗試10萬次,而你只是想讓它有時檢查一下。所以,你就可以設置一個Duration,它就會在每次醒來的時候嘗試做它的任務。

其他宏

我們再來看看其他一些宏。

unreachable!()

這個宏有點像todo!(),除了它是針對你永遠不會用的代碼。也許你在一個枚舉中有一個match,你知道它永遠不會選擇其中的一個分支,所以代碼永遠無法達到那個分支。如果是這樣,你可以寫unreachable!(),這樣編譯器就知道可以忽略這部分。

例如,假設你有一個程序,當你選擇一個地方居住時,它會寫一些東西。在烏克蘭,除了切爾諾貝利,其他地方都不錯。你的程序不讓任何人選擇切爾諾貝利,因為它現在不是一個好地方。但是這個枚舉是很早以前在別人的代碼裡做的,你無法更改。所以在match的分支中,你可以用這個宏。它是這樣的:

enum UkrainePlaces {
    Kiev,
    Kharkiv,
    Chernobyl, // Pretend we can't change the enum - Chernobyl will always be here
    Odesa,
    Dnipro,
}

fn choose_city(place: &UkrainePlaces) {
    use UkrainePlaces::*;
    match place {
        Kiev => println!("You will live in Kiev"),
        Kharkiv => println!("You will live in Kharkiv"),
        Chernobyl => unreachable!(),
        Odesa => println!("You will live in Odesa"),
        Dnipro => println!("You will live in Dnipro"),
    }
}

fn main() {
    let user_input = UkrainePlaces::Kiev; // Pretend the user input is made from some other function. The user can't choose Chernobyl, no matter what
    choose_city(&user_input);
}

這將打印出 You will live in Kiev

unreachable!()對你來說也很好讀,因為它提醒你代碼的某些部分是不可訪問的。不過你必須確定代碼確實是不可訪問的。如果編譯器調用unreachable!(),程序就會崩潰。

此外,如果你曾經有不可達的代碼,而編譯器知道,它會告訴你。下面是一個簡單的例子:

fn main() {
    let true_or_false = true;

    match true_or_false {
        true => println!("It's true"),
        false => println!("It's false"),
        true => println!("It's true"), // Whoops, we wrote true again
    }
}

它會說

warning: unreachable pattern
 --> src/main.rs:7:9
  |
7 |         true => println!("It's true"),
  |         ^^^^
  |

但是unreachable!()是用於編譯器無法知道的時候,就像我們另一個例子。

column!, line!, file!, module_path!

這四個宏有點像dbg!(),因為你只是把它們放進代碼去給你調試信息。但是它們不需要任何變量--你只需要用它們和括號一起使用,而沒有其他的東西。它們放到一起很容易學:

  • column!()給你寫的那一列
  • file!()給你寫的文件的名稱
  • line!()給你寫的那行字,然後是
  • module_path!()給你模塊的位置。

接下來的代碼在一個簡單的例子中展示了這三者。我們將假裝有更多的代碼(mod裡面的mod),因為這就是我們要使用這些宏的原因。你可以想象一個大的Rust程序,它有許多mod和文件。

pub mod something {
    pub mod third_mod {
        pub fn print_a_country(input: &mut Vec<&str>) {
            println!(
                "The last country is {} inside the module {}",
                input.pop().unwrap(),
                module_path!()
            );
        }
    }
}

fn main() {
    use something::third_mod::*;
    let mut country_vec = vec!["Portugal", "Czechia", "Finland"];

    // do some stuff
    println!("Hello from file {}", file!());

    // do some stuff
    println!(
        "On line {} we got the country {}",
        line!(),
        country_vec.pop().unwrap()
    );

    // do some more stuff

    println!(
        "The next country is {} on line {} and column {}.",
        country_vec.pop().unwrap(),
        line!(),
        column!(),
    );

    // lots more code

    print_a_country(&mut country_vec);
}

它打印的是這樣的。

Hello from file src/main.rs
On line 23 we got the country Finland
The next country is Czechia on line 32 and column 9.
The last country is Portugal inside the module rust_book::something::third_mod

cfg!

我們知道,你可以使用 #[cfg(test)]#[cfg(windows)] 這樣的屬性來告訴編譯器在某些情況下該怎麼做。當你有test時,當你在測試模式下運行Rust時,它會運行代碼(如果是在電腦上,你輸入cargo test)。而當你使用windows時,如果用戶使用的是Windows,它就會運行代碼。但也許你只是想根據不同操作系統對依賴系統的代碼做很小的修改。這時候這個宏就很有用了。它返回一個bool

fn main() {
    let helpful_message = if cfg!(target_os = "windows") { "backslash" } else { "slash" };

    println!(
        "...then in your hard drive, type the directory name followed by a {}. Then you...",
        helpful_message
    );
}

這將以不同的方式打印,取決於你的系統。Rust Playground在Linux上運行,所以會打印:

...then in your hard drive, type the directory name followed by a slash. Then you...

cfg!()適用於任何一種配置。下面是一個例子,當你在測試中使用一個函數時,它的運行方式會有所不同。

#[cfg(test)] // cfg! will know to look for the word test
mod testing {
    use super::*;
    #[test]
    fn check_if_five() {
        assert_eq!(bring_number(true), 5); // This bring_number() function should return 5
    }
}

fn bring_number(should_run: bool) -> u32 { // This function takes a bool as to whether it should run
    if cfg!(test) && should_run { // if it should run and has the configuration test, return 5
        5
    } else if should_run { // if it's not a test but it should run, print something. When you run a test it ignores println! statements
        println!("Returning 5. This is not a test");
        5
    } else {
        println!("This shouldn't run, returning 0."); // otherwise return 0
        0
    }
}

fn main() {
    bring_number(true);
    bring_number(false);
}

現在根據配置的不同,它的運行方式也會不同。如果你只是運行程序,它會給你這樣的結果:

Returning 5. This is not a test
This shouldn't run, returning 0.

但如果你在測試模式下運行它(cargo test,用於電腦上的Rust),它實際上會運行測試。因為在這種情況下,測試總是返回5,所以它會通過。

running 1 test
test testing::check_if_five ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

編寫宏

編寫宏是非常複雜的。你可能永遠都不需要寫宏,但有時你可能會想寫,因為它們非常方便。寫宏很有趣,因為它們幾乎是不同的語言。要寫一個宏,你實際上是用另一個叫macro_rules!的宏。然後你添加你的宏名稱,並打開一個{}塊。裡面有點像match語句。

這裡有一個只取(),然後返回6:

macro_rules! give_six {
    () => {
        6
    };
}

fn main() {
    let six = give_six!();
    println!("{}", six);
}

但這和match語句是不一樣的,因為宏實際上不會編譯任何東西。它只是接受一個輸入並給出一個輸出。然後編譯器會檢查它是否有意義。這就是為什麼宏就像 "寫代碼的代碼"。你會記得,一個真正的match語句需要給出相同的類型,所以這不會工作:

fn main() {
// ⚠️
    let my_number = 10;
    match my_number {
        10 => println!("You got a ten"),
        _ => 10,
    }
}

它會抱怨你在一種情況下要返回(),在另一種情況下要返回i32

error[E0308]: `match` arms have incompatible types
 --> src\main.rs:5:14
  |
3 | /     match my_number {
4 | |         10 => println!("You got a ten"),
  | |               ------------------------- this is found to be of type `()`
5 | |         _ => 10,
  | |              ^^ expected `()`, found integer
6 | |     }
  | |_____- `match` arms have incompatible types

但宏並不關心,因為它只是給出一個輸出。它不是一個編譯器--它是代碼前的代碼。所以你可以這樣做:

macro_rules! six_or_print {
    (6) => {
        6
    };
    () => {
        println!("You didn't give me 6.");
    };
}

fn main() {
    let my_number = six_or_print!(6);
    six_or_print!();
}

這個就好辦了,打印的是You didn't give me 6.。你也可以看到,這不是匹配分支,因為沒有_行。我們只能給它(6),或者(),其他的都會出錯。而我們給它的6甚至不是i32,只是一個輸入6。其實你可以設置任何東西作為宏的輸入,因為它只是看輸入,看得到什麼。比如說:

macro_rules! might_print {
    (THis is strange input 하하はは哈哈 but it still works) => {
        println!("You guessed the secret message!")
    };
    () => {
        println!("You didn't guess it");
    };
}

fn main() {
    might_print!(THis is strange input 하하はは哈哈 but it still works);
    might_print!();
}

所以這個奇怪的宏只響應兩件事。()(THis is strange input 하하はは哈哈 but it still works). 沒有其他的東西。它打印的是:

You guessed the secret message!
You didn't guess it

所以宏不完全是Rust語法。但是宏也可以理解你給它的不同類型的輸入。拿這個例子來說。

macro_rules! might_print {
    ($input:expr) => {
        println!("You gave me: {}", $input);
    }
}

fn main() {
    might_print!(6);
}

這將打印You gave me: 6$input:expr部分很重要。它的意思是 "對於一個表達式,給它起一個變量名$input"。在宏中,變量以$開頭。在這個宏中,如果你給它一個表達式,它就會打印出來。我們再來試一試。

macro_rules! might_print {
    ($input:expr) => {
        println!("You gave me: {:?}", $input); // Now we'll use {:?} because we will give it different kinds of expressions
    }
}

fn main() {
    might_print!(()); // give it a ()
    might_print!(6); // give it a 6
    might_print!(vec![8, 9, 7, 10]); // give it a vec
}

這將打印:

You gave me: ()
You gave me: 6
You gave me: [8, 9, 7, 10]

另外注意,我們寫了{:?},但它不會檢查&input是否實現了Debug。它只會寫代碼,並嘗試讓它編譯,如果沒有,那麼它就會給出一個錯誤。

那麼除了expr,宏還能看到什麼呢?它們是 block | expr | ident | item | lifetime | literal | meta | pat | path | stmt | tt | ty | vis. 這就是複雜的部分。你可以在這裡看到它們各自的意思,這裡說:

item: an Item
block: a BlockExpression
stmt: a Statement without the trailing semicolon (except for item statements that require semicolons)
pat: a Pattern
expr: an Expression
ty: a Type
ident: an IDENTIFIER_OR_KEYWORD
path: a TypePath style path
tt: a TokenTree (a single token or tokens in matching delimiters (), [], or {})
meta: an Attr, the contents of an attribute
lifetime: a LIFETIME_TOKEN
vis: a possibly empty Visibility qualifier
literal: matches -?LiteralExpression

另外有一個很好的網站叫cheats.rs,在這裡解釋了它們,並且每個都給出了例子。

然而,對於大多數宏,你只會用到 expridentttident 表示標識符,用於變量或函數名稱。tt表示token樹,和任何類型的輸入。讓我們嘗試用這兩個詞創建一個簡單的宏。

macro_rules! check {
    ($input1:ident, $input2:expr) => {
        println!(
            "Is {:?} equal to {:?}? {:?}",
            $input1,
            $input2,
            $input1 == $input2
        );
    };
}

fn main() {
    let x = 6;
    let my_vec = vec![7, 8, 9];
    check!(x, 6);
    check!(my_vec, vec![7, 8, 9]);
    check!(x, 10);
}

所以這將取一個ident(像一個變量名)和一個表達式,看看它們是否相同。它的打印結果是

Is 6 equal to 6? true
Is [7, 8, 9] equal to [7, 8, 9]? true
Is 6 equal to 10? false

而這裡有一個宏,輸入tt,然後把它打印出來。它先用一個叫stringify!的宏創建一個字符串。

macro_rules! print_anything {
    ($input:tt) => {
        let output = stringify!($input);
        println!("{}", output);
    };
}

fn main() {
    print_anything!(ththdoetd);
    print_anything!(87575oehq75onth);
}

這個將打印:

ththdoetd
87575oehq75onth

但是如果我們給它一些帶有空格、逗號等的東西,它就不會打印。它會認為我們給了它不止一個元素或額外的信息,所以它會感到困惑。

這就是宏開始變得困難的地方。

要一次給宏提供多個元素,我們必須使用不同的語法。不要用$input,而是$($input1),*。這意味著零或更多(這是 * 的意思),用逗號分隔。如果你想要一個或多個,請使用 + 而不是 *

現在我們的宏看起來是這樣的。

macro_rules! print_anything {
    ($($input1:tt),*) => {
        let output = stringify!($($input1),*);
        println!("{}", output);
    };
}


fn main() {
    print_anything!(ththdoetd, rcofe);
    print_anything!();
    print_anything!(87575oehq75onth, ntohe, 987987o, 097);
}

所以它接受任何用逗號隔開的token樹,並使用 stringify! 把它變成一個字符串。然後打印出來。它的打印結果是:

ththdoetd, rcofe

87575oehq75onth, ntohe, 987987o, 097

如果我們使用+而不是*,它會給出一個錯誤,因為有一次我們沒有給它輸入。所以*是一個比較安全的選擇。

所以現在我們可以開始看到宏的威力了。在接下來的這個例子中,我們實際上可以創建我們自己的函數:

macro_rules! make_a_function {
    ($name:ident, $($input:tt),*) => { // First you give it one name for the function, then it checks everything else
        fn $name() {
            let output = stringify!($($input),*); // It makes everything else into a string
            println!("{}", output);
        }
    };
}


fn main() {
    make_a_function!(print_it, 5, 5, 6, I); // We want a function called print_it() that prints everything else we give it
    print_it();
    make_a_function!(say_its_nice, this, is, really, nice); // Same here but we change the function name
    say_its_nice();
}

這個打印:

5, 5, 6, I
this, is, really, nice

所以現在我們可以開始瞭解其他的宏了。你可以看到,我們已經使用的一些宏非常簡單。這裡是我們用來寫入文件的write!的那個:

#![allow(unused)]
fn main() {
macro_rules! write {
    ($dst:expr, $($arg:tt)*) => ($dst.write_fmt($crate::format_args!($($arg)*)))
}
}

要使用它,你就輸入這個:

  • 一個表達式(expr) 得到變量名$dst.
  • 之後的一切。如果它寫的是$arg:tt,那麼它只會取1個,但是因為它寫的是$($arg:tt)*,所以它取0,1,或者任意多個。

然後它取$dst,並對它使用了一個叫做write_fmt的方法。在這裡面,它使用了另一個叫做format_args!的宏,它接受所有的$($arg)*,或者我們輸入的所有參數。

現在我們來看一下todo!這個宏。當你想讓程序編譯但還沒有寫出你的代碼時,就會用到這個宏。它看起來像這樣:

#![allow(unused)]
fn main() {
macro_rules! todo {
    () => (panic!("not yet implemented"));
    ($($arg:tt)+) => (panic!("not yet implemented: {}", $crate::format_args!($($arg)+)));
}
}

這個有兩個選項:你可以輸入(),也可以輸入一些token樹(tt)。

  • 如果你輸入(),它只是panic!,並加上一個信息。所以其實你可以直接寫panic!("not yet implemented"),而不是todo!,這也是一樣的。
  • 如果你輸入一些參數,它會嘗試打印它們。你可以看到裡面有同樣的format_args!宏,它的工作原理和println!一樣。

所以,如果你寫了這個,它也會工作:

fn not_done() {
    let time = 8;
    let reason = "lack of time";
    todo!("Not done yet because of {}. Check back in {} hours", reason, time);
}

fn main() {
    not_done();
}

這將打印:

thread 'main' panicked at 'not yet implemented: Not done yet because of lack of time. Check back in 8 hours', src/main.rs:4:5

在一個宏裡面,你甚至可以調用同一個宏。這裡有一個。

macro_rules! my_macro {
    () => {
        println!("Let's print this.");
    };
    ($input:expr) => {
        my_macro!();
    };
    ($($input:expr),*) => {
        my_macro!();
    }
}

fn main() {
    my_macro!(vec![8, 9, 0]);
    my_macro!(toheteh);
    my_macro!(8, 7, 0, 10);
    my_macro!();
}

這個可以取(),也可以取一個表達式,也可以取很多表達式。但是不管你放什麼表達式,它都會忽略所有的表達式,只是在()上調用my_macro!。所以輸出的只是Let's print this,四次。

dbg!宏中也可以看到同樣的情況,也是調用自己。

#![allow(unused)]
fn main() {
macro_rules! dbg {
    () => {
        $crate::eprintln!("[{}:{}]", $crate::file!(), $crate::line!()); //$crate means the crate that it's in.
    };
    ($val:expr) => {
        // Use of `match` here is intentional because it affects the lifetimes
        // of temporaries - https://stackoverflow.com/a/48732525/1063961
        match $val {
            tmp => {
                $crate::eprintln!("[{}:{}] {} = {:#?}",
                    $crate::file!(), $crate::line!(), $crate::stringify!($val), &tmp);
                tmp
            }
        }
    };
    // Trailing comma with single argument is ignored
    ($val:expr,) => { $crate::dbg!($val) };
    ($($val:expr),+ $(,)?) => {
        ($($crate::dbg!($val)),+,)
    };
}
}

(eprintln!println!相同,只打印到io::stderr而不是io::stdout。還有eprint!不增加一行)。)

所以我們可以自己去試一試。

fn main() {
    dbg!();
}

這與第一分支相匹配,所以它會用file!line!宏打印文件名和行名。它打印的是[src/main.rs:2]

我們用這個來試試。

fn main() {
    dbg!(vec![8, 9, 10]);
}

這將匹配下一個分支,因為它是一個表達式。然後它將調用輸入tmp並使用這個代碼。 $crate::eprintln!("[{}:{}] {} = {:#?}", $crate::file!(), $crate::line!(), $crate::stringify!($val), &tmp);. 所以它會用file!line!來打印,然後把$val做成String,用{:#?}來漂亮的打印tmp。所以對於我們的輸入,它會這樣寫。

[src/main.rs:2] vec![8, 9, 10] = [
    8,
    9,
    10,
]

剩下的部分,即使你多加了一個逗號,它也只是自己調用dbg!

正如你所看到的,宏是非常複雜的!通常你只想讓一個宏自動完成一些簡單函數不能很好完成的事情。學習宏的最好方法是看其他宏的例子。沒有多少人能夠快速寫出宏而不出問題。所以不要認為你需要知道宏的一切,才能知道如何在Rust中寫。但如果你讀了其他宏,並稍加修改,你就可以很容易地借用它們的力量。然後你可能會開始適應寫自己的宏。

第2部分 - 電腦上的Rust

你看到了,我們幾乎可以使用Playground學習Rust中的任何東西。但如果你到目前為止已經學了這麼多,現在你可能會想要在你的電腦上使用Rust。總有一些事情是你不能用Playground做的,比如使用文件或代碼在多個文件中。其他如輸入和flags也需要在電腦上安裝Rust。但最重要的是,在你的電腦上有了Rust,你可以使用Crate。我們已經瞭解了crate,但在playground中你只能使用最流行的crate。但在你的電腦上,你可以在程序中使用任何crate。

cargo

rustc的意思是Rust編譯器,實際的編譯工作由它完成。一個rust文件的結尾是.rs。但大多數人不會寫出類似 rustc main.rs 的東西來編譯。他們使用的是名為 cargo 的東西,它是 Rust 的主包管理器。

關於這個名字的一個說明: 之所以叫cargo,是因為當你把crate放在一起時,你會得到cargo。Crate就是你在船上或卡車上看到的木箱,但你記住,每個Rust項目也叫Crate。那麼當你把它們放在一起時,你就會得到整個cargo。

當你使用cargo來運行一個項目時,你可以看到這一點。讓我們用 rand 試試簡單的東西:我們只是在八個字母之間隨機選擇。

use rand::seq::SliceRandom; // Use this for .choose over slices

fn main() {

    let my_letters = vec!['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];

    let mut rng = rand::thread_rng();
    for _ in 0..6 {
        print!("{} ", my_letters.choose(&mut rng).unwrap());
    }
}

這樣就會打印出b c g h e a這樣的東西。但我們想先看看cargo的作用。要使用 cargo 並運行我們的程序,通常我們輸入 cargo run。這樣就可以構建我們的程序,併為我們運行它。當它開始編譯的時候,會做這樣的事情:

   Compiling getrandom v0.1.14
   Compiling cfg-if v0.1.10
   Compiling ppv-lite86 v0.2.8
   Compiling rand_core v0.5.1
   Compiling rand_chacha v0.2.2
   Compiling rand v0.7.3
   Compiling rust_book v0.1.0 (C:\Users\mithr\OneDrive\Documents\Rust\rust_book)
    Finished dev [unoptimized + debuginfo] target(s) in 13.13s
     Running `C:\Users\mithr\OneDrive\Documents\Rust\rust_book\target\debug\rust_book.exe`
g f c f h b

所以看起來不只是引入了rand,還引入了一些其他的crate。這是因為我們的crate需要rand,但rand也有一些代碼也需要其他crate。所以cargo會找到我們需要的所有crate,並把它們放在一起。在我們的案例中,只有7個,但在非常大的項目中,你可能會有200個或更多的crate要引入。

這就是你可以看到Rust的權衡的地方。Rust的速度非常快,因為它提前編譯。它通過查看代碼,看你寫的代碼到底做了什麼。例如,你可能會寫這樣的泛型代碼:

use std::fmt::Display;

fn print_and_return_thing<T: Display>(input: T) -> T {
    println!("You gave me {} and now I will give it back.", input);
    input
}

fn main() {
    let my_name = print_and_return_thing("Windy");
    let small_number = print_and_return_thing(9.0);
}

這個函數可以用任何實現了Display的作為參數,所以我們給它一個&str,接下來給它一個f64,這對我們來說是沒有問題的。但是編譯器不看泛型,因為它不想在運行時做任何事情。它想把一個能運行的程序儘可能快地組裝起來。所以當它看第一部分的"Windy"時,它沒有看到fn print_and_return_thing<T: Display>(input: T) -> T,它看到的是fn print_and_return_thing(input: &str) -> &str這樣的東西。而接下來它看到的是fn print_and_return_thing(input: f64) -> f64。所有關於trait的檢查等等都是在編譯時完成的。這就是為什麼泛型需要更長的時間來編譯,因為它需要弄清楚它們,並使之具體化。

還有一點:Rust 2020正在努力處理編譯時間問題,因為這部分需要的時間最長。每一個版本的Rust在編譯時都會快一點,而且還有一些其他的計劃來加快它的速度。但與此同時,以下是你應該知道的:

  • cargo build會構建你的程序,這樣你就可以運行它了。
  • cargo run將建立你的程序並運行它。
  • cargo build --releasecargo run --release發佈模式下有同樣的效果。什麼是發佈模式?當你的代碼最終完成後就可以用發佈模式了。Rust會花更多的時間來編譯,但它這樣做是因為它使用了它所知道的一切,來使編譯出的程序運行更快。Release模式實際上比常規模式快的多,常規模式被稱為debug模式。那是因為它的編譯速度更快,而且有更多的調試信息。常規的cargo build叫做 "debug build",cargo build --release叫做 "release build"。
  • cargo check是一種檢查代碼的方式。它就像編譯一樣,只不過它不會真正地創建你的程序。這是一個很好的檢查你的代碼的方法,因為它不像buildrun那樣需要很長時間。

對了,命令中的--release部分叫做flag。這意味著命令中的額外信息。

其他一些你需要知道的事情是:

  • cargo new. 這樣做是為了創建一個新的Rust項目。new之後,寫上項目的名稱,cargo將會創建所有你需要的文件和文件夾。
  • cargo clean. 當你把crate添加到Cargo.toml時,電腦會下載所有需要的文件,它們會佔用很多空間。如果你不想再讓它們在你的電腦上,輸入cargo clean

關於編譯器還有一點:只有當你第一次使用cargo buildcargo run時,它才會花費最多的時間。之後它就會記住,它又會快速編譯。但如果你使用 cargo clean,然後運行 cargo build,它將不得不再慢慢地編譯一次。

接受用戶輸入

一個簡單的方法是用std::io::stdin來接受用戶的輸入。這意味著 "標準輸入",也就是來自鍵盤的輸入。用stdin()可以獲得用戶的輸入,但是接下來你就會想用.read_line()把它放到&mut String中。下面是一個簡單的例子,但它既能工作,也不能工作:

use std::io;

fn main() {
    println!("Please type something, or x to escape:");
    let mut input_string = String::new();

    while input_string != "x" { // This is the part that doesn't work right
        input_string.clear(); // First clear the String. Otherwise it will keep adding to it
        io::stdin().read_line(&mut input_string).unwrap(); // Get the stdin from the user, and put it in read_string
        println!("You wrote {}", input_string);
    }
    println!("See you later!");
}

下面是一個輸出輸出的樣子。

Please type something, or x to escape:
something
You wrote something

Something else
You wrote Something else

x
You wrote x

x
You wrote x

x
You wrote x

它接受我們的輸入,然後把它還給我們,它甚至知道我們輸入了x。但它並沒有退出程序。唯一的辦法是關閉窗口,或者輸入ctrl和c。讓我們把println!中的{}改為{:?},以獲得更多的信息(如果你喜歡那個宏,也可以使用dbg!(&input_string))。現在它說

Please type something, or x to escape:
something
You wrote "something\r\n"
Something else
You wrote "Something else\r\n"
x
You wrote "x\r\n"
x
You wrote "x\r\n"

這是因為鍵盤輸入其實不只是something,而是somethingEnter鍵。有一個簡單的方法可以解決這個問題,叫做.trim(),它可以把所有的空白都去掉。順便說一下,這些字符都是空白字符。

U+0009 (horizontal tab, '\t')
U+000A (line feed, '\n')
U+000B (vertical tab)
U+000C (form feed)
U+000D (carriage return, '\r')
U+0020 (space, ' ')
U+0085 (next line)
U+200E (left-to-right mark)
U+200F (right-to-left mark)
U+2028 (line separator)
U+2029 (paragraph separator)

這樣就可以把x\r\n變成只剩x了。現在它可以工作了:

use std::io;

fn main() {
    println!("Please type something, or x to escape:");
    let mut input_string = String::new();

    while input_string.trim() != "x" {
        input_string.clear();
        io::stdin().read_line(&mut input_string).unwrap();
        println!("You wrote {}", input_string);
    }
    println!("See you later!");
}

現在可以打印了:

Please type something, or x to escape:
something
You wrote something

Something
You wrote Something

x
You wrote x

See you later!

還有一種用戶輸入叫std::env::Args(env是環境的意思)。Args是用戶啟動程序時輸入的內容。其實在一個程序中總是至少有一個Arg。我們寫一個程序,只用std::env::args()來打印它們,看看它們是什麼。

fn main() {
    println!("{:?}", std::env::args());
}

如果我們寫cargo run,那麼它的打印結果是這樣的:

Args { inner: ["target\\debug\\rust_book.exe"] }

讓我們給它更多的輸入,看看它的作用。我們輸入 cargo run but with some extra words 。 它給我們:

Args { inner: ["target\\debug\\rust_book.exe", "but", "with", "some", "extra", "words"] }

有意思。而當我們查看Args的頁面時,我們看到它實現了IntoIterator。這意味著我們可以.用所有我們知道的關於迭代器的方法來讀取和改變它。讓我們試試這個:

use std::env::args;

fn main() {
    let input = args();

    for entry in input {
        println!("You entered: {}", entry);
    }
}

現在它說:

You entered: target\debug\rust_book.exe
You entered: but
You entered: with
You entered: some
You entered: extra
You entered: words

你可以看到,第一個參數總是程序名,所以你經常會想跳過它,比如這樣:

use std::env::args;

fn main() {
    let input = args();

    input.skip(1).for_each(|item| {
        println!("You wrote {}, which in capital letters is {}", item, item.to_uppercase());
    })
}

這將打印:

You wrote but, which in capital letters is BUT
You wrote with, which in capital letters is WITH
You wrote some, which in capital letters is SOME
You wrote extra, which in capital letters is EXTRA
You wrote words, which in capital letters is WORDS

Args的一個常見用途是用於用戶設置。你可以確保用戶寫出你需要的輸入,只有在正確的情況下才運行程序。這裡有一個小程序,可以讓字母變大(大寫)或變小(小寫)。

use std::env::args;

enum Letters {
    Capitalize,
    Lowercase,
    Nothing,
}

fn main() {
    let mut changes = Letters::Nothing;
    let input = args().collect::<Vec<_>>();

    if input.len() > 2 {
        match input[1].as_str() {
            "capital" => changes = Letters::Capitalize,
            "lowercase" => changes = Letters::Lowercase,
            _ => {}
        }
    }

    for word in input.iter().skip(2) {
      match changes {
        Letters::Capitalize => println!("{}", word.to_uppercase()),
        Letters::Lowercase => println!("{}", word.to_lowercase()),
        _ => println!("{}", word)
      }
    }

}

下面是它給出的一些例子。

輸入: cargo run please make capitals:

make capitals

輸入:cargo run capital:

// Nothing here...

輸入:cargo run capital I think I understand now:

I
THINK
I
UNDERSTAND
NOW

輸入:cargo run lowercase Does this work too?

does
this
work
too?

除了用戶給出的 Args,在 std::env::args() 中可用,還有系統變量Vars。這些都是用戶沒有輸入的程序的基本設置。你可以用std::env::vars()把它們都看成一個(String, String)。這個有非常多,比如說:

fn main() {
    for item in std::env::vars() {
        println!("{:?}", item);
    }
}

運行這段代碼,就能顯示出你的用戶會話的所有信息。它將顯示這樣的信息:

("CARGO", "/playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/cargo")
("CARGO_HOME", "/playground/.cargo")
("CARGO_MANIFEST_DIR", "/playground")
("CARGO_PKG_AUTHORS", "The Rust Playground")
("CARGO_PKG_DESCRIPTION", "")
("CARGO_PKG_HOMEPAGE", "")
("CARGO_PKG_NAME", "playground")
("CARGO_PKG_REPOSITORY", "")
("CARGO_PKG_VERSION", "0.0.1")
("CARGO_PKG_VERSION_MAJOR", "0")
("CARGO_PKG_VERSION_MINOR", "0")
("CARGO_PKG_VERSION_PATCH", "1")
("CARGO_PKG_VERSION_PRE", "")
("DEBIAN_FRONTEND", "noninteractive")
("HOME", "/playground")
("HOSTNAME", "f94c15b8134b")
("LD_LIBRARY_PATH", "/playground/target/debug/build/backtrace-sys-3ec4c973f371c302/out:/playground/target/debug/build/libsqlite3-sys-fbddfbb9b241dacb/out:/playground/target/debug/build/ring-cadba5e583648abb/out:/playground/target/debug/deps:/playground/target/debug:/playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib:/playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib")
("PATH", "/playground/.cargo/bin:/playground/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")
("PLAYGROUND_EDITION", "2018")
("PLAYGROUND_TIMEOUT", "10")
("PWD", "/playground")
("RUSTUP_HOME", "/playground/.rustup")
("RUSTUP_TOOLCHAIN", "stable-x86_64-unknown-linux-gnu")
("RUST_RECURSION_COUNT", "1")
("SHLVL", "1")
("SSL_CERT_DIR", "/usr/lib/ssl/certs")
("SSL_CERT_FILE", "/usr/lib/ssl/certs/ca-certificates.crt")
("USER", "playground")
("_", "/usr/bin/timeout")

所以如果你需要這些信息,Vars就是你想要的。

獲得單個Var'的最簡單方法是使用env!宏。你只要給它變量的名字,它就會給你一個&str'的值。如果變量拼寫錯誤或不存在,它就不起作用,所以如果你不確定,就用option_env!代替。如果我們在Playground上寫這個:

fn main() {
    println!("{}", env!("USER"));
    println!("{}", option_env!("ROOT").unwrap_or("Can't find ROOT"));
    println!("{}", option_env!("CARGO").unwrap_or("Can't find CARGO"));
}

然後我們得到輸出:

playground
Can't find ROOT
/playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/cargo

所以option_env!永遠是比較安全的宏。如果你真的想讓程序在找不到環境變量時崩潰,那麼env!會更好。

使用文件

現在我們在電腦上使用Rust,我們可以開始處理文件了。你會注意到,現在我們會開始在代碼中越來越多的看到Result。這是因為一旦你開始處理文件和類似的事情,很多事情都會出錯。一個文件可能不在那裡,或者計算機無法讀取它。

你可能還記得,如果你想使用?運算符,調用它的函數必須返回一個Result。如果你記不住錯誤類型,你可以什麼都不給它,讓編譯器告訴你。讓我們用一個試圖用.parse()創建一個數字的函數來試試。

// ⚠️
fn give_number(input: &str) -> Result<i32, ()> {
    input.parse::<i32>()
}

fn main() {
    println!("{:?}", give_number("88"));
    println!("{:?}", give_number("5"));
}

編譯器告訴我們到底該怎麼做。

error[E0308]: mismatched types
 --> src\main.rs:4:5
  |
3 | fn give_number(input: &str) -> Result<i32, ()> {
  |                                --------------- expected `std::result::Result<i32, ()>` because of return type
4 |     input.parse::<i32>()
  |     ^^^^^^^^^^^^^^^^^^^^ expected `()`, found struct `std::num::ParseIntError`
  |
  = note: expected enum `std::result::Result<_, ()>`
             found enum `std::result::Result<_, std::num::ParseIntError>`

很好! 所以我們只要把返回值改成編譯器說的就可以了:

use std::num::ParseIntError;

fn give_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() {
    println!("{:?}", give_number("88"));
    println!("{:?}", give_number("5"));
}

現在程序可以運行了!

Ok(88)
Ok(5)

所以現在我們想用?,如果能用就直接給我們數值,如果不能用就給錯誤。但是如何在fn main()中做到這一點呢?如果我們嘗試在main中使用?,那就不行了。

// ⚠️
use std::num::ParseIntError;

fn give_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() {
    println!("{:?}", give_number("88")?);
    println!("{:?}", give_number("5")?);
}

它說:

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `std::ops::Try`)
  --> src\main.rs:8:22
   |
7  | / fn main() {
8  | |     println!("{:?}", give_number("88")?);
   | |                      ^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `()`
9  | |     println!("{:?}", give_number("5")?);
10 | | }
   | |_- this function should return `Result` or `Option` to accept `?`

但實際上main()可以返回一個Result,就像其他函數一樣。如果我們的函數能工作,我們不想返回任何東西(main()並沒有給其他任何東西)。而如果它不工作,我們將錯誤返回。所以我們可以這樣寫:

use std::num::ParseIntError;

fn give_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() -> Result<(), ParseIntError> {
    println!("{:?}", give_number("88")?);
    println!("{:?}", give_number("5")?);
    Ok(())
}

不要忘了最後的Ok(()):這在Rust中是很常見的,它的意思是Ok,裡面是(),也就是我們的返回值。現在它打印出來了:

88
5

只用.parse()的時候不是很有用,但是用文件就很有用。這是因為?也為我們改變了錯誤類型。下面是用簡單英語寫的?運算符頁面:

If you get an `Err`, it will get the inner error. Then `?` does a conversion using `From`. With that it can change specialized errors to more general ones. The error it gets is then returned.

另外,Rust在使用Files和類似的東西時,有一個方便的Result類型。它叫做std::io::Result,當你在使用?對文件進行打開和操作時,通常在main()中看到的就是這個。這其實是一個類型別名。它的樣子是這樣的:

type Result<T> = Result<T, Error>;

所以這是一個Result<T, Error>,但我們只需要寫出Result<T>部分。

現在讓我們第一次嘗試使用文件。std::fs是處理文件的方法所在,有了std::io::Write,你就可以寫。有了它,我們就可以用.write_all()來寫進文件。

use std::fs;
use std::io::Write;

fn main() -> std::io::Result<()> {
    let mut file = fs::File::create("myfilename.txt")?; // Create a file with this name.
                                                        // CAREFUL! If you have a file with this name already,
                                                        // it will delete everything in it.
    file.write_all(b"Let's put this in the file")?;     // Don't forget the b in front of ". That's because files take bytes.
    Ok(())
}

然後如果你打開新文件myfilename.txt,會看到內容Let's put this in the file

不過我們不需要寫兩行,因為我們有?操作符。如果有效,它就會傳遞我們想要的結果,有點像在迭代器上很多方法一樣。這時候?就變得非常方便了。

use std::fs;
use std::io::Write;

fn main() -> std::io::Result<()> {
    fs::File::create("myfilename.txt")?.write_all(b"Let's put this in the file")?;
    Ok(())
}

所以這是說 "請嘗試創建一個文件,然後檢查是否成功。如果成功了,那就使用.write_all(),然後檢查是否成功。"

而事實上,也有一個函數可以同時做這兩件事。它叫做std::fs::write。在它裡面,你給它你想要的文件名,以及你想放在裡面的內容。再次強調,要小心! 如果該文件已經存在,它將刪除其中的所有內容。另外,它允許你寫一個&str,前面不寫b,因為這個:

#![allow(unused)]
fn main() {
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()>
}

AsRef<[u8]>就是為什麼你可以給它任何一個。

很簡單的:

use std::fs;

fn main() -> std::io::Result<()> {
    fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;

    Ok(())
}

所以這就是我們要用的文件。這是一個名叫Calvin的漫畫人物和他爸爸的對話,他爸爸對他的問題並不認真。有了這個,每次我們都可以創建一個文件來使用。

打開一個文件和創建一個文件一樣簡單。你只要用open()代替create()就可以了。之後(如果它找到了你的文件),你就可以做read_to_string()這樣的事情。要做到這一點,你可以創建一個可變的 String,然後把文件讀到那裡。它看起來像這樣:

use std::fs;
use std::fs::File;
use std::io::Read; // this is to use the function .read_to_string()

fn main() -> std::io::Result<()> {
     fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;


    let mut calvin_file = File::open("calvin_with_dad.txt")?; // Open the file we just made
    let mut calvin_string = String::new(); // This String will hold it
    calvin_file.read_to_string(&mut calvin_string)?; // Read the file into it

    calvin_string.split_whitespace().for_each(|word| print!("{} ", word.to_uppercase())); // Do things with the String now

    Ok(())
}

會打印:

#![allow(unused)]
fn main() {
CALVIN: DAD, HOW COME OLD PHOTOGRAPHS ARE ALWAYS BLACK AND WHITE? DIDN'T THEY HAVE COLOR FILM BACK THEN? DAD: SURE THEY DID. IN 
FACT, THOSE PHOTOGRAPHS *ARE* IN COLOR. IT'S JUST THE *WORLD* WAS BLACK AND WHITE THEN. CALVIN: REALLY? DAD: YEP. THE WORLD DIDN'T TURN COLOR UNTIL SOMETIMES IN THE 1930S...
}

好吧,如果我們想創建一個文件,但如果已經有另一個同名的文件就不做了怎麼辦?也許你不想為了創建一個新的文件而刪除已經存在的其他文件。要做到這一點,有一個結構叫OpenOptions。其實,我們一直在用OpenOptions,卻不知道。看看File::open的源碼吧。

#![allow(unused)]
fn main() {
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
        OpenOptions::new().read(true).open(path.as_ref())
    }
}

有意思,這好像是我們學過的建造者模式。File::create也是如此。

#![allow(unused)]
fn main() {
pub fn create<P: AsRef<Path>>(path: P) -> io::Result<File> {
        OpenOptions::new().write(true).create(true).truncate(true).open(path.as_ref())
    }
}

如果你去OpenOptions的頁面,你可以看到所有可以選擇的方法。大多數採取bool

  • append(): 意思是 "添加到已經存在的內容中,而不是刪除"。
  • create(): 這讓 OpenOptions 創建一個文件。
  • create_new(): 意思是隻有在文件不存在的情況下才會創建文件。
  • read(): 如果你想讓它讀取文件,就把這個設置為 true
  • truncate(): 如果你想在打開文件時把文件內容剪為0(刪除內容),就把這個設置為true。
  • write(): 這可以讓它寫入一個文件。

然後在最後你用.open()加上文件名,就會得到一個Result。我們來看一個例子。

// ⚠️
use std::fs;
use std::fs::OpenOptions;

fn main() -> std::io::Result<()> {
     fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;

    let calvin_file = OpenOptions::new().write(true).create_new(true).open("calvin_with_dad.txt")?;

    Ok(())
}

首先我們用new做了一個OpenOptions(總是以new開頭)。然後我們給它的能力是write。之後我們把create_new()設置為true,然後試著打開我們做的文件。打不開,這是我們想要的。

Error: Os { code: 80, kind: AlreadyExists, message: "The file exists." }

讓我們嘗試使用.append(),這樣我們就可以向一個文件寫入。為了寫入文件,我們可以使用 .write_all(),這是一個嘗試寫入你給它的一切內容的方法。

另外,我們將使用 write! 宏來做同樣的事情。你會記得這個宏,我們在為結構體做impl Display的時候用到過。這次我們是在文件上使用它,而不是在緩衝區上。

use std::fs;
use std::fs::OpenOptions;
use std::io::Write;

fn main() -> std::io::Result<()> {
    fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;

    let mut calvin_file = OpenOptions::new()
        .append(true) // Now we can write without deleting it
        .read(true)
        .open("calvin_with_dad.txt")?;
    calvin_file.write_all(b"And it was a pretty grainy color for a while too.\n")?;
    write!(&mut calvin_file, "That's really weird.\n")?;
    write!(&mut calvin_file, "Well, truth is stranger than fiction.")?;

    println!("{}", fs::read_to_string("calvin_with_dad.txt")?);

    Ok(())
}

這個打印:

Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...And it was a pretty grainy color for a while too.
That's really weird.
Well, truth is stranger than fiction.

cargo文檔

你可能已經注意到,Rust文檔看起來總是幾乎一樣。在左邊你可以看到structtrait,代碼例子在右邊等等。這是因為你只要輸入cargo doc就可以自動創建文檔。

即使是創建一個什麼都不做的項目,也可以幫助你瞭解Rust中的特性。例如,這裡有兩個幾乎什麼都不做的結構體,以及一個也什麼都不做的fn main()

struct DoesNothing {}
struct PrintThing {}

impl PrintThing {
    fn prints_something() {
        println!("I am printing something");
    }
}

fn main() {}

但如果你輸入cargo doc --open,你可以看到比你想象中更多的信息。首先它給你顯示的是這樣的:

Crate rust_book

Structs
DoesNothing
PrintThing

Functions
main

但是如果你點擊其中的一個結構,會讓你看到很多你沒有想到的trait。

Struct rust_book::DoesNothing
[+] Show declaration
Auto Trait Implementations
impl RefUnwindSafe for DoesNothing
impl Send for DoesNothing
impl Sync for DoesNothing
impl Unpin for DoesNothing
impl UnwindSafe for DoesNothing
Blanket Implementations
impl<T> Any for T
where
    T: 'static + ?Sized,
[src]
[+]
impl<T> Borrow<T> for T
where
    T: ?Sized,
[src]
[+]
impl<T> BorrowMut<T> for T
where
    T: ?Sized,
[src]
[+]
impl<T> From<T> for T
[src]
[+]
impl<T, U> Into<U> for T
where
    U: From<T>,
[src]
[+]
impl<T, U> TryFrom<U> for T
where
    U: Into<T>,
[src]
[+]
impl<T, U> TryInto<U> for T
where
    U: TryFrom<T>,

這是因為Rust自動為每個類型實現的所有trait。

那麼如果我們添加一些文檔註釋,當你輸入cargo doc的時候就可以看到。

/// This is a struct that does nothing
struct DoesNothing {}
/// This struct only has one method.
struct PrintThing {}
/// It just prints the same message.
impl PrintThing {
    fn prints_something() {
        println!("I am printing something");
    }
}

fn main() {}

現在會打印:

Crate rust_book
Structs
DoesNothing This is a struct that does nothing
PrintThing  This struct only has one method.
Functions
main

當你使用很多別人的crate時,cargo doc是非常好的。因為這些crate都在不同的網站上,可能需要一些時間來搜索所有的crate。但如果你使用cargo doc,你就會把它們都放在你硬盤的同一個地方。

結束了嗎?

簡單英語學Rust就這樣結束了。但是我還在這裡,如果你有什麼問題可以告訴我。歡迎在Twitter上聯繫我或者添加一個pull request、issue等。如果有些地方不容易理解,你也可以告訴我。簡單英語學Rust需要非常容易理解,所以請告訴我英語太難的地方。當然,Rust本身也可能是很難理解的,但我們至少可以確保英語是容易的。

Rust-安裝環境

安裝rustup

rustup是rust版本管理器

安裝指令如下

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

接著安裝rust總共有三個版可以選擇

  • stable
  • beta
  • nightly
rustup install stable

輸入下面指令檢查版本,如果有顯示就是代表安裝成功

rustc --version
rustc 1.54.0 (a178d0322 2021-07-26)

也可以透過這個 ~/.cargo/bin 目錄查看到安裝的rust工具

ls ~/.cargo/bin

cargo cargo-clippy cargo-fmt cargo-miri clippy-driver rls rust-gdb rust-lldb rustc rustdoc rustfmt rustup

升級rust

rustup update

切換rust版本

查看目前所以安裝的版本

ls ~/.rustup/toolchains/

beta-x86_64-apple-darwin
stable-x86_64-apple-darwin

切換預設版本

rustup default beta-x86_64-apple-darwin

info: using existing install for 'beta-x86_64-apple-darwin'
info: default toolchain set to 'beta-x86_64-apple-darwin'

  beta-x86_64-apple-darwin unchanged - rustc 1.55.0-beta.9 (27e88d367 2021-08-28)

針對專案設定版本

rustup override set nightly

info: using existing install for 'beta-x86_64-apple-darwin'
info: override toolchain for '/Users/ken_jan/rust/hello' set to 'beta-x86_64-apple-darwin'

  beta-x86_64-apple-darwin unchanged - rustc 1.55.0-beta.9 (27e88d367 2021-08-28)

卸載rust

rustup self uninstall

如果你只是想嘗試看看,完成不想安裝,也可以使用瀏覽線上版本

Rust Playground

相對於golang的安裝,rust官方就內建版本控制方便許多

這部分rust大勝

Rust-編輯器設定

如何設定開發環境這邊使用VSCode來當開發工具

rustfmt 自動格式化

rustfmt是Rust官方提供自動格式化代碼的工具,用來統一代碼風格,避免有人用Tab有人用空格來縮排或是在大括號之後該換行之類的

透過rustup安裝

#![allow(unused)]
fn main() {
rustup component add rustfmt
}

透過指令來格式化代碼

#![allow(unused)]
fn main() {
rustfmt main.rs
}

也可以透過cargo格式化整個專案

#![allow(unused)]
fn main() {
cargo fmt
}

也可以單純檢查並列出沒排好的地放

#![allow(unused)]
fn main() {
rustfmt --check main.rs

cargo fmt -- --check 
}

預設的rustfmt風格就很好用了,但如果你的團隊或是個人習慣想要不一樣排版風格也是可以的,透過rustfmt.toml設定檔來改變

例如二元運算子多行時要放在頭還是放在尾的部分

#![allow(unused)]
fn main() {
binop_separator = "Front" (默認)

let or = foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo
        || barbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar;

---------------------------------------------------------------
binop_separator = "Back"

let or = foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo ||
        barbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar;
}

還有其他很多設定,可以參考這文件

https://github.com/rust-lang/rustfmt/blob/master/Configurations.md

讓vscode支援rust代碼提示,需要安裝Rust套件模組

https://marketplace.visualstudio.com/items?itemName=rust-lang.rust

安裝時會順便安裝rls, rust-src, and rust-analysis

如果要讓存檔時自動格式化代碼需要修改

File > Preferences > Settings. 裡面的 editor.formatOnSave 打勾 就可以了

Rust-Hello, World!

先建立一個hello的目錄,編輯main.rs

fn main() {
    print!("Hello, World!");
}

儲存然後在Terminal用rustc編譯

#![allow(unused)]
fn main() {
rustc main.rs
}

會產生出main檔案,執行main就會出現Hello, world!了

./main
Hello, World!

分析程式碼

  • fu是rust的關鍵字,function的簡寫
  • main是函式名稱,執行rust程式時將執行,如果沒有main哪可能是一個library
  • print! 是rust的巨集(macro) 如果沒有"!"表示是函示有的話則是巨集
  • "Hello, world!" 表示字串內容
  • 最後用";"表示語法結束

rust對大小寫是敏感的

利用"{}"站位符輸出字串,類似golang的fmt.Printf("%s, %s!", "Hello", "World")

#![allow(unused)]
fn main() {
print!("{}, {}!", "Hello", "World")

輸出
Hello, World!
}

利用"{}"站位符輸出數字,類似golang的fmt.Printf("%s: %d", "Num", 9527)

#![allow(unused)]
fn main() {
print!("{}: {}", "Num", 9527)

輸出
Num: 9527
}

用"\n"輸出多行字串

#![allow(unused)]
fn main() {
print!("Hello, World!\nHello, World!\nHello, World!")

輸出
Hello, World!
Hello, World!
Hello, World!
}

Rust-變數

變數宣告

#![allow(unused)]
fn main() {
// 宣告區域變數
let local_var = 123;
}

不可變變數

#![allow(unused)]
fn main() {
let immutable_var = 123;

print!("{}", immutable_var);

immutable_var = 456;

print!("{}", immutable_var);
}

上面這段程式碼在編譯時會出現"cannot assign twice to immutable variable"錯誤,表示不可變變數無法重新賦予值

有點類似golang中的const(常數)但又不太一樣,rust本身也有const關鍵字

可變變數

#![allow(unused)]
fn main() {
let mut mutable_var = 123;

print!("{}", mutable_var);

mutable_var = 456;

print!("{}", mutable_var);
}

透過在宣告變時增加mut(mutable)來讓這個變數可以重新賦予值

未變化的可變變數

#![allow(unused)]
fn main() {
let mut mutable_var = 123;

print!("{}", mutable_var);
}

如果宣告了可變變數,但是後面又沒重新賦予值時編譯會出現"help: remove this mut"警告來建議移除mut

未初始化的變數

#![allow(unused)]
fn main() {
let immutable_var :i32;

print!("{}", immutable_var);
}

編譯時會出現"use of possibly-uninitialized immutable_var"錯誤

初始化變數也可以在宣告變數之後,只要在變數使用之前初始化就可以,例如下面例子

#![allow(unused)]
fn main() {
let immutable_var :i32;

immutable_var = 123;

print!("{}", immutable_var);
}

型別和可變化的改變

rust允許在變數宣告後又重新宣告相同名稱的變數,下面這些行為在rust是合法

#![allow(unused)]
fn main() {
let mut var = 123;

print!("{}", var);

var = 456;

print!("{}", var);

// 重新宣告為不可變變數
let var = 789;

print!("{}", var);

// 重新宣告為字串類型
let var = "hello word";

print!("{}", var);
}

未使用的變數

#![allow(unused)]
fn main() {
let immutable_var = 123;
}

未使用變數時會出現"help: if this is intentional, prefix it with an underscore: _immutable_var"警告,如果不想出現警告可以在變數前面加個下底線"_"

#![allow(unused)]
fn main() {
let _immutable_var = 123;
}

或是單純只是要一個站位符也可以這樣

#![allow(unused)]
fn main() {
let _ = 123;
}

Rust-值,變數,物件

不應將值,變數,物件混淆為一體

單詞"值"表示抽象的數學概念,

例如值:"9527"是指數學9527的數學概念,在數學上"9527"只會有一個"9527"的數字

例如值:"hello, word!"從概念上也只會有一個

值可以存在電腦裡的記憶體裡,可以在記憶體的多個位置儲存數字"9527",可以有兩個不同位置都儲放"9257"

在記憶體中包含值的部分稱為物件,兩個位於記憶體中不同位置的不同物件如果包含相同的內容則可以稱為它們"相等"

在編譯Rust時,生成可以執行的程式會包含具有存儲位置和值的物件,這些物件沒有名稱 但是在程式碼中會希望將名稱與物件相關聯,以便以後可以引用它們這個東西可以稱為變數

例如

#![allow(unused)]
fn main() {
let num = 9527;
print!("{}", num);

輸出9257
}

第一行表示:

  • 它在記憶中劃出一個足夠大的物件,以包含一個整數
  • 它以二進制格式將值"9527"儲存在該物件中
  • 它將名稱num與該物件做關聯,以便之後在程式碼中使用該名稱num來指示這個物件

建立識別字 (Identifier) 的規則

變數名稱又稱為識別字。識別字原本在 Rust 程式中是沒有意義的,透過宣告變數這項動作對特定識別字賦予關聯。

Rust 的識別字採用以下規則:

  • 第一個字元為英文或底線 _
  • 第二個之後的字元為英文、數字或底線
  • 只有單一的底線 _ 不是變數

以下是合 Rust 規範的變數名稱:

  • a
  • a1
  • a_var
  • aVar
  • _var

Rust建議使用蛇行(snake case)命名(例:a_var_snake_name)

而非駝峰(camel case)命名(例:aVarCamelName)

跟golang顛倒,沒有好壞只要統一風格就好

Rust會對不符合其撰碼風格的變數或函式名稱發出警告訊息,但不會引發錯誤

Rust-資料型別-整數、浮點數

Rust是靜態型別語言,所以在編譯時需要知道變數的型別是什麼

前面的程式範例很多是沒有宣吿型別但是卻可以編譯,這邊用到的是透過

通常編譯器能通過數值來推導型別是什麼

Rust 有四種主要純量型別:整數、浮點數、布林以及字元

整數型別

整數是沒有小數點的數字,分有帶號(signed)跟非帶號(unsigned),差別就是一個有負值一個沒有負值

帶號範圍-(2^n - 1) 到 (2^n - 1) - 1

非帶號範圍0到2^n - 1

isize跟usize則是依據運行環境的電腦是32位元還是還64位元決定大小

Rust預設整數型別是i32

溢位問題

Rust在執行時會檢查是否有溢位

#![allow(unused)]
fn main() {
let mut n: i32 = i32::max_value();
 
// Overflow
n = n + 1;

出現panic錯誤
thread 'main' panicked at 'attempt to add with overflow'
}

如果你想要讓溢位也視為正常的可以在編譯時增加參數

#![allow(unused)]
fn main() {
// rustc用法
rustc -O main.rs

// cargo用法
cargo build --release
}

浮點數型別

浮點數只有兩種型別

  • f32 32位元大小
  • f64 64位元大小

浮點數是依照 IEEE-754

Rust預設浮點數型別是f64

Rust-資料型別-布林值

Rust 為了表示真假值,使用關鍵字true和false

這樣的關鍵字具有非數字類型的表達式稱為布林

#![allow(unused)]
fn main() {
let true_var = true;
let false_var = false;
print!("{} {}", true_var, false_var);

輸出
true false
}

除了關鍵字true和false賦予值也可以透過比較運算表達式來賦予值

#![allow(unused)]
fn main() {
let true_var = 456 > 123 ;
let false_var = -456 >= 123;
print!("{} {} {}", true_var, false_var, -123 < 123);

輸出
true false true
}

比較運算子如下:

  • ==:等於

  • !=:不等於

  • <:小於

  • :大於

  • <=:小於或是等於

  • =:大於或是等於

跟常見語言都一樣

除了比較數字之外也能比較字串

#![allow(unused)]
fn main() {
print!("{} {} {}", "efg" < "efgh", "efg" < "efh", "D" < "d");

輸出 true true true
}

字串的比較規則就是比較兩個字串的第一個字母,然後繼續比較兩個字串相同位置的字母直到發生下列情況之一:

  • 如果兩個字母都沒有其他字母了,則相等
  • 如果一個字串沒有其他字母,而另一個還有其他字母,則較少字母的字串較小
  • 如果兩個字串相同位置的字母比較,字母表在前的比較小,字母大寫比字母小寫小

邏輯運算子如下:

  • !:not
  • &&:且
  • ||:或
#![allow(unused)]
fn main() {
let true_var = true;
let false_var = false;

print!("{} {}", !true_var, !false_var);
輸出
false true

print!("{} {} {} {}", true_var && true_var, false_var && false_var, true_var && false_var, false_var && true_var);
輸出
true false false false

print!("{} {} {} {}", true_var || true_var, false_var || false_var, true_var || false_var, false_var || true_var);
輸出
true false true true
}

Rust-資料型別-字元.字串

Rust的char型別是最基本的字母型別,用單引號包起來

#![allow(unused)]
fn main() {
let a = 'b';
}

Rust的字串分兩種

  • str
  • String

嚴格來講Rust在核心語言中只有一個字串型別,哪就是切片字串(&str)是不可變的(stack)

String則是Rust標準函式庫提供的型別,String是可變(Heap)

#![allow(unused)]
fn main() {
// &str型別
let str_var = "字串";

// String型別
let string_var = str_var.to_string();
// String型別
let string_var = String::from("字串");
}

索引字串切片

因為字串是使用 UTF-8 編碼,每個字符可能有多個位元組所以用字串索引會容易出現錯誤

#![allow(unused)]
fn main() {
let str_var = "安安";

println!("{}", &str_var[0..1]);

會出現錯誤
thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside '安' (bytes 0..3) of `安安`'
}

比較好得做法透過as_bytes或是chars方法來取直

#![allow(unused)]
fn main() {
let str_var = "安安你好啊";

for b in str_var.as_bytes() {
    print!("{}, ", b);
}

for c in str_var.chars() {
    print!("{}, ", c);
}

輸出
229, 174, 137, 229, 174, 137, 228, 189, 160, 229, 165, 189, 229, 149, 138,
安, 安, 你, 好, 啊,
}

字串相加,這邊會使用到&借用,後面會講解

#![allow(unused)]
fn main() {
let string1_var = String::from("安安");
let string2_var = String::from("你好啊");

println!("{}", string1_var + &string2_var);

輸出
安安你好啊
}

Rust沒有提供型別判斷的方法,如果想要知道變數是什麼型別可以利用編譯時的錯來查看

#![allow(unused)]
fn main() {
// char型別
let char_var = 'c';
char_var.not_found;

出現錯誤
`char` is a primitive type and therefore doesn't have fields

// &str型別
let str_var = "str";
str_var.not_found;

出現錯誤
no field `not_found` on type `&str`
}

Rust-資料型別-複合型別

複合型別是指多個數值組為一個型別

Rust 有兩個基本複合型別

元組型別(tupl)

元組型別是指將多個不同型別組成一個複合型別

固定長度,宣告好就無法增減長度

每一格都是一個獨立型別

#![allow(unused)]
fn main() {
// 宣告時就指定型別
let tup: (i32, f64, u8, &str) = (1, 3.1417, 1, "hello");

// 自動判斷型別的宣告方式
let tup = (1, 3.1417, 1, "hello");

println!("{:?}", tup);

輸出
(1, 3.1417, 1, "hello")

// 多維元組
let tup = ("1", ("2", ("3")));
}

元組也可以透過解構(destructuring)拆分各個型別

#![allow(unused)]
fn main() {
let tup: (i32, f64, u8, &str) = (1, 3.1417, 1, "hello");

// 解構成各個變數
let (a, b, c, d) = tup;

println!("{} {} {} {}", a, b, c, d);

輸出
1 3.1417 1 hello
}

也可以透過"."再加上索引位置來取得元組內的值

#![allow(unused)]
fn main() {
let tup: (i32, f64, u8, &str) = (1, 3.1417, 1, "hello");

// 索引從0開始
println!("{} {} {} {}", tup.0, tup.1, tup.2, tup.3);

輸出
1 3.1417 1 hello
}

陣列型別(array)

和元組一樣固定長度,差別是陣列中的型別必須要一樣的

#![allow(unused)]
fn main() {
// 宣告時指定型別和長度i32是型別;之後的5是陣列的長度
let array: [i32; 5] = [1, 2, 3, 4, 5];

// 自動判斷型別的宣告方式
let array = [1, 2, 3, 4, 5];

// 簡化的宣告方式,陣列裡全是hello長度為5
let array = ["hello"; 5];
}

透果索引位址取值的方式

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];

println!("{} {} {} {} {}", a[0], a[1], a[2], a[3], a[4]);

輸出
1 2 3 4 5
}

Rust-流程控制-if

利用布林值來決定如何繼續執行程式進行決策

#![allow(unused)]
fn main() {
let n = 3;

if n > 2 {
    println!("執行")
}
}

if 跟其他語言差不多,if關鍵字後面布林求值稱為條件只有當true才會執行大括號裡面的語法

大括號裡面可以包含零個多個語法,用大括號包起來的稱為塊(block)

條件必須是布林類型,因此不允許下面的語法

#![allow(unused)]
fn main() {
// 不允許
if 1 {
   print!("執行");
}
}

條件不需要用小括號包起來,會出現警告

#![allow(unused)]
fn main() {
// 出現警告
if (3 > 2) {
   println!("執行")
}
}

條件之後需要一個塊(block)包起來

#![allow(unused)]
fn main() {
// 不允許
i 3 > 2 println!("執行")
}

如果要在條件為false情況執行流程可以使用else關鍵字

#![allow(unused)]
fn main() {
let n = 3;

if n > 5 {
    println!("執行")
} else 
    println!("false執行")
}

輸出
false執行
}

Rust也有類似PHP的三元運算

#![allow(unused)]
fn main() {
let n = 3;

let str = if n > 2 { "true_str" } else { "false_str" };

println!("{}", str);

輸出
true_str
}

以下範例是不允許的

#![allow(unused)]
fn main() {
// 不允許,因為無法定義str的型別
let str = if true { "true_str" }

// 不允許,因為型別不一樣,一個是字串一個是數字
let str = if true { "true_str" } else { 9527 }
}

Rust-流程控制-while

類似PHP的while迴圈,計算其後的布林條件如果是值為true則執行大括號下面的語法,會重複條件的檢查執行直到條件值為false為止或是其他原因退出

假設要打印1到100的整數可以使用下面的語法來達到

#![allow(unused)]
fn main() {
let mut i = 1;

while i <= 100 {
    print!("{} ", i);
    i += 1;
}

輸出
1 2 3 4 5 ... 100
}

雖然Rust沒有do while的語法但也有break contiune語法

例如只想印出雙數

#![allow(unused)]
fn main() {
let mut i = 1;

 while i <= 100 {
     i += 1;
     if i % 2 != 0 {
        continue;
     }
     print!("{} ", i);
}

輸出
2 4 6 8 10 ... 100
}

或是碰到50的值就中斷退出

#![allow(unused)]
fn main() {
let mut i = 1;

while i <= 100 {
    if i == 50 {
        break;
    }
    print!("{} ", i);
		i += 1;
}

輸出
1 2 3 4 5 ... 49
}

無限循環(loop)

如果要執行無限循環的迴圈直到程式被強制中斷或是通過退出循環語法break,可以透過loop語法

#![allow(unused)]
fn main() {
// 透過whilce 會出現警告
let mut i = 1;

while true {
    if i == 50 {
        break;
    }
    i += 1;
}

// 透過loop
let mut i = 1;

loop {
    if i == 50 {
         break;
    }
    i += 1;
}
}

Rust-流程控制-for

如果想印出1到100的數字,更常的做法是使用for迴圈而不是while

#![allow(unused)]
fn main() {
for i in 1..101 {
    println!("{}", i);
}

輸出
1 2 3 4 5 ... 100
}

for和in是關鍵字在用兩個數字加".."符號分隔

i變數一開始由第一個數字(1)賦予該值,然後i值依序的加1直到值達到第二數字(101),當值到達第二數是"不"執行的並結束for迴圈,所以要1到100需要寫1..101

i是for迴圈的區域變數所以前面如果有宣告都會被忽略,而且在迴圈結束就銷毀

#![allow(unused)]
fn main() {
let i = 99; // 被屏蔽的
for i in 0..11 {
		// 這裡的i是區域變數
    print!("{} ", i);
}
// 這裡的i是一開始宣告的99
print!("{}", i);

輸出
0 1 2 3 4 5 6 7 8 9 10 99
}

for迴圈也可以用來遍歷集合的每個元素

#![allow(unused)]
fn main() {
let set = [1, 2, 3, 4, 5];

for var in set {
    print!("{} ", var);
}

輸出
1 2 3 4 5
}

或是反轉範圍從100印到1

#![allow(unused)]
fn main() {
for var in (1..101).rev() {
    print!("{} ", var);
}

輸出
100 99 98 97 96 ... 1
}

Rust-枚舉(enumeration)

枚舉就是列出有窮序列的型別

透過enum關鍵字新增了新的Browser型別在範例中列出了一個組項分別為

Firefox,Chrome,IE,Safari內部值分別為0u8,1u8,2u8,3u8表示

通常都是以整數為內部關聯值

透過枚舉寫以下的代碼

#![allow(unused)]
fn main() {
enum Browser {
    Firefox,
    Chrome,
    Ie,
    Safari,
}

let browser = Browser::Ie;

match browser {
    Browser::Firefox => println!("F"),
    Browser::Chrome => println!("C"),
    Browser::Ie => println!("I"),
    Browser::Safari => println!("S"),
}
}

不要編寫這樣的代碼

#![allow(unused)]
fn main() {
const FIREFOX: u8 = 0;
const CHROME: u8 = 1;
const IE: u8 = 2;
const SAFARI: u8 = 3;

let browser = IE;

if browser == FIREFOX {
    println!("F");
} else if browser == CHROME {
    println!("C");
} else if browser == IE {
    println!("I");
} else if browser == SAFARI {
    println!("S");
}
}

再怎樣也不要寫magic number

如何使用枚舉

使用Use的方式

#![allow(unused)]
fn main() {
// 顯式的指定要使用的枚舉
use Browser::{Chrome, Firefox, Ie, Safari};

let browser = Ie;

match browser {
    Firefox => println!("F"),
    Chrome => println!("C"),
    Ie => println!("I"),
    Safari => println!("S"),
}

// 自動的使用Browser內部所有的枚舉
use Browser::*;

let browser = Ie;

match browser {
    Firefox => println!("F"),
    Chrome => println!("C"),
    Ie => println!("I"),
    Safari => println!("S"),
}
}

使用帶有C風格的用法

#![allow(unused)]
fn main() {
enum Browser {
    Firefox,
    Chrome,
    Ie,
    Safari,
}

println!("{}", Browser::Firefox as i32)
}

枚舉不能使用"=="運算子做比較

#![allow(unused)]
fn main() {
let browser = Browser::Ie;

if browser == Browser::Ie {
    println!("{}", "hello word")
}

// 編譯時會出錯
// binary operation `==` cannot be applied to type `Browser`
}

Rust-Match控制流運算子

Match是使用枚舉的基本工具,類似Golang的Switch語法

Match取值後對每個條件進行比較依照順序比較,一但匹配成功就對右側求值,並結算Match語法

每個條件分支右側都必須是單個表達式

前面三個都是無效的表達式

#![allow(unused)]
fn main() {
match browser {
    Firefox => let var = 777;, // 無效的表達式
    Chrome => let var = 777, // 無效的表達式
    Ie =>  fu funct() {}, // 無效的表達式
    Safari => println!("S"),
}
}

如果需要在右側好幾個表達式或是非表達式可以用塊來包起來

#![allow(unused)]
fn main() {
enum Browser {
    Firefox,
    Chrome,
    Ie,
    Safari,
}

let browser = Browser::Ie;

match browser {
    Browser::Firefox => {
        browser = Browser::Ie;
        println!("F");
    },
    Browser::Chrome => {
        let var = 777;
        println!("C")
    },
    Browser::Ie => println!("I"),
    Browser::Safari => println!("S"),
}
}

處理所有可能

#![allow(unused)]
fn main() {
enum Browser {
    Firefox,
    Chrome,
    Ie,
    Safari,
}

let browser = Browser::Ie;

match browser {
    Browser::Ie => println!("I"),
    Browser::Safari => println!("S"),
}

// 編譯時會出
patterns `Firefox` and `Chrome` not covered
}

因為少了Firefox和Chrome條件分支,Rust要求mtch需要顯式的處理所有可能的情況

可以使用空處理

#![allow(unused)]
fn main() {
let browser = Browser::Ie;

match browser {
    Browser::Firefox => {}, // 使用空處理
    Browser::Chrome => {}, // 使用空處理
    Browser::Ie => println!("I"),
    Browser::Safari => println!("S"),
}
}

或是使用"_"下底線,類似Golang的Default

#![allow(unused)]
fn main() {
let browser = Browser::Ie;

match browser {
    Browser::Ie => println!("I"),
    Browser::Safari => println!("S"),
    _ => {},
}
}

如果Default放在條件最上面會因為順序關係什麼都不處理

#![allow(unused)]
fn main() {
let browser = Browser::Ie;

match browser {
    _ => {}, // 永遠都跑到這裡
    Browser::Ie => println!("I"),
    Browser::Safari => println!("S"),
}
}

對型別使用Match

match除了對枚舉之外也可以對一般型別

#![allow(unused)]
fn main() {
match "value" {
    "value" => println!("value"),
    _ => println!("other"),
}

match 1 {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    _ => println!("other"),
}
}

因為型別不像枚舉有可窮舉出所有,所以一定需要"_"(Default) 條件,不然編譯會出現錯誤

Rust-結構體(Struct)

struct 是命名並封裝數個欄位數值所組合的自訂型別

struct 有 3 種類型

  • 元組結構體(tuple struct),就是具名元組而已
  • 經典的C語言風格結構體(C struct)
  • 單元結構體(unit struct),不帶字段,在泛型中很有用

前幾篇所講的元組型別(tupl),只要元組包含少量的字段就很好用但是當字段一多就容易將它們搞混 並且代碼也很容易理解

例如下面的代碼,不能很輕易的知道哪個字段相加,而且元組型別是有序的如果在開頭增加字段哪變成後面的字段都需要往後遞延非常不直覺

#![allow(unused)]
fn main() {
let data = (1, 'a', 'b', 199.199, true, -100);
println!("{}", data.0 + data.5);
}

這時候使用結構體(Struct)就非常有用,可以為字段命名並標示型別

它以struct關鍵字開頭後面接著要聲明的類型再以大括號包起來

例如

#![allow(unused)]
fn main() {
// 經典的C語言風格結構體(C struct)
struct User {
    username: String,
    email: String,
    active: bool,
}

let user = User {
    email: String::from("user@usermail.com"),
    username: String::from("user123"),
    active: true,
};

println!("{} {} {}", user.email, user.username, user.active)
}

元組結構,擁有元組型別和結構兩個特性又不完全一樣

  • 需要事先聲明
  • 字段沒有名稱

實際上這種結構不常使用

#![allow(unused)]
fn main() {
struct User(String, String, bool);

let user = User(
    String::from("user@usermail.com"),
    String::from("user123"),
    true,
);

println!("{} {} {}", user.0, user.1, user.2)
}

單元結構體

單元結構體適合用在當要實作一個特徵(trait)或某種型別,但沒有任何需要儲存在型別中的資料

#![allow(unused)]
fn main() {
struct AlwaysEqual;

let subject = AlwaysEqual;
}

Rust-定義函式Function(一)

如果編寫多次相同的代碼,則可以把代碼封裝在一個塊中,然後為該代碼命名

通過這種方式就定義了函式,然後可以通過命名的名稱來調用該函式

要訂一個函式需要使用"fn"關鍵字後面接著函式的名稱跟圓括號然後是一個大括號塊

大括號的塊稱為函式體,函式體之前都稱為簽名

下面例子簡單使用函式

#![allow(unused)]
fn main() {
fn hello_word() {
    println!("hello word!")
}

hello_word();
hello_word();
hello_word();

輸出
hello word!
hello word!
hello word!
}

後定義函式

#![allow(unused)]
fn main() {
a;
let a = 5; // 非法使用變數

hello_word(); // 合法使用後定義的函式
fn hello_word() {
    println!("hello word!")
}
}

函式屏蔽其他函式

#![allow(unused)]
fn main() {
fn hello_word() {}
fn hello_word() {} // 多次定義函式

編譯時會出錯
the name `hello_word` is defined multiple times
}

但是可以包在塊裡多次定義fn

#![allow(unused)]
fn main() {
{
    fn hello_word() {
        println!("hello word 1")
    }

    hello_word();
}
{
    fn hello_word() {
        println!("hello word 2")
    }

    hello_word();
}

輸出
hello word 1
hello word 2
}

每個定義函式只能在塊裡面有效,下面是不合法的

#![allow(unused)]
fn main() {
{
    fn hello_word() {
        println!("hello word 2")
    }
}

hello_word();

編譯時會出錯
cannot find function `hello_word` in this scope
}

也可以在屏蔽外的塊級定義另一個函式

在main外部定義函式hello_word(),因其內部也定義了hello_word,所以永遠用不到外部的定義函式

通常編譯器會警告

fn hello_word() {
    println!("hello word 1")
}

fn main() {
    hello_word();

    {
        hello_word();
        fn hello_word() {
            println!("hello word 2")
        }
    }

    hello_word();
    fn hello_word() {
        println!("hello word 3")
    }
}

這邊輸出

hello word 3
hello word 2
hello word 3

Rust-定義函式Function(二)

函式傳遞參數

每次調用函式時都打印相同的hello word的函式不是很有用處

這時候可以傳遞參數給函式會顯得更有意義

#![allow(unused)]
fn main() {
fn hello_word(name: String) {
    println!("{} hello", name)
}

hello_word(String::from("Mike"));

輸出
Mike hello
}

函式參數的定義與變數定義非常相似

因此可以將面的函式解釋為

#![allow(unused)]
fn main() {
{
    let name: String = String::from("Mike");
    println!("{} hello", name)
}
}

變數的定義與函式參數的定義主要的差別在於函式定義需要明確的指定類型

變數的定義可以依賴類型推斷

編譯器會使用類型推斷來檢查參數值是否合法

#![allow(unused)]
fn main() {
fn f(c: i16) {}

f(5.); // 非法因為是浮點數
f(5u16); // 非法因為是u16型別
f(5i16); // 合法
f(5); // 合法因為傳遞是無約束得整數類型參數會變函式限制為i16類型
}

函式按"值"傳遞參數

參數不僅僅是傳遞物件的新名稱是傳遞物件的副本,此副本在調用函式時創建並在函式結束且控制返回到調用者時銷毀它

例如下面

#![allow(unused)]
fn main() {
fn test(mut f: f64) {
    f *= 10.;
    println!("{}", f)
}

let f = 5.;
test(f);
println!("{}", f);

輸出
50
5
}

在一開始就宣告的f變數在傳遞給test函式的,並在函式中保留使用f變數並更改變數得值並打印,函式結束反給調用者,然侯在印出f變數,這個變數與調用時一樣

實際上傳遞給函式的不是這個變數而是變數的"值"並在函式結尾時銷毀

函式返回值

函式除了能接收參數之外還可以計算結果並返回給調用者

#![allow(unused)]
fn main() {
fn test(f: f64) -> f64 {
    return f * 10.;
}

println!("{}", test(5.));
}

返回值需要再函式簽名之後用"→"並指定型別

Rust-定義函式Function(三)

提前退出

正常情況是必須達到函示的末尾,但是如果編寫包含許多語法的函式通常會在意識到沒有更多計算要做時因此需要提前退出該函式

#![allow(unused)]
fn main() {
fn f(x: i32) -> i32 {
    if x <= 0 {
        return 0;
    }

    return x * 5;
}

println!("{} {}", f(10), f(0))

輸出
50 0
}

返回多個值

如果要從函式返回多個值,可以使用元組型別

#![allow(unused)]
fn main() {
fn f(x: i32) -> (i32, i32) {
    return (x * 5, 10);
}

println!("{:?}", f(10))

輸出
(50, 10)
}

或是也可以用返回結構,元組結構,數組或是向量來返回多個

更改調用者的變數

假設有個包含數字的數組,要更改其中的數字

#![allow(unused)]
fn main() {
fn f(mut a: [i32; 5]) {
    for i in 0..5 {
        if a[i] > 0 {
            a[i] *= 2;
        }
    }
}

let mut arr = [5, -1, 2, -2, 8];
f(arr);
println!("{:?}", arr);

輸出
[5, -1, 2, -2, 8]
}

可以看到並沒有改變數組,並且在編譯時出現警告variable does not need to be mutable

告訴我們mut聲明arr之後卻沒有改變可以移除

比較麻煩的作法,可以透過回傳值回傳一個新的數組,缺點就是數組會複製兩次

#![allow(unused)]
fn main() {
fn f(mut a: [i32; 5]) -> [i32; 5] {
    for i in 0..5 {
        if a[i] > 0 {
            a[i] *= 2;
        }
    }

    return a;
}

let mut arr = [5, -1, 2, -2, 8];
arr = f(arr);
println!("{:?}", arr);

輸出
[10, -1, 4, -2, 16]
}

可以透過引用傳遞參數的方式來改變數組

#![allow(unused)]
fn main() {
fn f(a: &mut [i32; 5]) {
    for i in 0..5 {
        if (*a)[i] > 0 {
            (*a)[i] *= 2;
        }
    }
}

let mut arr = [5, -1, 2, -2, 8];
f(&mut arr);
println!("{:?}", arr);

輸出
[10, -1, 4, -2, 16]
}

透過&符號來表示物件記憶體位址,而使用*符號表示記憶體位址的物件

通過型別宣告&mut [i32; 5]指定它是物件的地址(可以稱為指針Pointer或是引用reference)

這邊的使用跟C++使用很像,區別是Rust還允許顯式的取消引用可以省略*符號

這個函式等同於上面

#![allow(unused)]
fn main() {
fn f(a: &mut [i32; 5]) {
    for i in 0..5 {
        if a[i] > 0 { // 省略了*
            a[i] *= 2; // 省略了*
        }
    }
}

let mut arr = [5, -1, 2, -2, 8];
f(&mut arr);
println!("{:?}", arr);

輸出
[10, -1, 4, -2, 16]
}

Rust-定義泛型函式

Rust是強型別語言,執行嚴格的資料型別檢查,因此當定義使用某種型別參數的函式時比如說

#![allow(unused)]
fn main() {
square(x: f32) -> f32
}

調用函式的程式碼必須傳遞一個嚴格屬於這種型別的表達式例如

#![allow(unused)]
fn main() {
square(1.3414f32)
}

或者是每次使用該函式時都執行顯式的型別轉換例如

#![allow(unused)]
fn main() {
square(1.3414f64 as f32)
}

對於使用的人非常不方便,對於編寫該函式的人也不方便

由於Rust有很多數字類的型別,如果決定了參數型別是i32型別但每次調用幾乎都是用i64哪最好是更改參數型別為i64,而且如果函式有多個模塊或是多個程式在使用則很難滿足每個調用者的需求

例如

#![allow(unused)]
fn main() {
fn f(s: char, n1: i16, n2: i16) -> i16 {
    if s == '1' {
        return n1;
    }

    return n2;
}

println!("{}", f('1', 10, 20));

輸出
10
}

這時候想用f32當參數肯定是無法使用這函式,但總不能為了這個需求又寫了邏輯一樣的函式只差在參數不同,這時候就可以使用泛型函式了

定義泛型函式

fn main() {
		println!("{}", f::<i16>('1', 10, 20));
    println!("{}", f::<f32>('1', 10.1, 20.1));
}

fn f<T>(s: char, n1: T, n2: T) -> T {
    if s == '1' {
        return n1;
    }

    return n2;
}

輸出
10
10.1

這個函式既可以輸入i16也可以輸入f32當參數了

在定義函式中,函式名之後用<>包起來字母T,該字母為函式宣告的型別參數

這個表示宣告的不是具體函式而是由T型別參數來參數化的泛型函式,只有在編譯時為該T參數指定具體型別時該函式才成為具體函式

在使用泛型函式需要將T參數替換成實際使用f::的型別來護得具體函式

上面例子有三個地方宣告成T型別,在使用時需要三個都是相同型別不然在編譯時會出現錯誤

推斷參數型別

可以透過Rust的型別推斷簡化成下面使用範例

fn main() {
    println!("{}", f('1', 10, 20)); // 透別推斷型別簡化
    println!("{}", f('1', 10.1, 20.1)); // 透別推斷型別簡化
}

fn f<T>(s: char, n1: T, n2: T) -> T {
    if s == '1' {
        return n1;
    }

    return n2;
}

Rust-定義泛型結構

既然有泛型函數當然少不瞭泛型結構

#![allow(unused)]
fn main() {
struct S1<T1, T2> {
   n1: T1,
   n2: T2,
}

let s = S {n1:957, n2:996.1}
}

第一句語法宣告了兩個型別T1和T2參數化的泛型結構S1,第一個字段n1為T1第二字段n2為T2

第二句語法n1字段的參數隱式的替換成i32,n2字段的參數隱式替換成f64

也可以使用泛型元組結構

#![allow(unused)]
fn main() {
struct SE<T1, T2> (T1, T2)

let se = SE (957, 996.1)
}

泛型機制

通過下面程式碼來理解編譯泛型機制概念

#![allow(unused)]
fn main() {
fn swap<T1, T2>(p1: T1, p2: T2) -> (T2, T1) {
    return (p2, p1);
}

let x = swap(5i16, 9u16);
let y = swap(6f32, true);

println!("{:?} {:?}", x, y);

輸出
(9, 5) (true, 6.0)
}

第一階段編譯器會掃描程式碼並且每次找到泛型函式宣告(swap)時,它在資料結構中加載該函式內部表示形式檢查泛型程式碼是否有語法錯誤

第二階段編譯器再次掃描程式碼在每次遇到泛型函式調用時,編譯器都會檢查此類用法與泛型宣告的相應內部表示關聯,在確認後再其資料結構中加載這種對應關係

第三階段再將掃描所有泛型函式調用,對於每個泛型函式調用者都定義每個泛型參數的具體型別,這種具體型別可以是在用法中顯式的或者如上面範例一樣用推斷出來的

在確定替換泛型參數的具體型別之後都會產生泛型函式的具體版本對應程式碼如下

#![allow(unused)]
fn main() {
fn swap_i16_u16(p1: i16, p2: u16) -> (u16, i16) {
    return (p2, p1);
}

fn swap_f32_bool(p1: f32, p2: bool) -> (f32, bool) {
    return (p2, p1);
}

let x = swap_i16_u16(5i16, 9u16);
let y = swap_f32_bool(6f32, true);

println!("{:?} {:?}", x, y);
}

泛型編譯有幾個特點

  • 與非泛型程式碼相比多階段編譯相對較慢一些
  • 生成的程式碼針對每個特定的調用進行高度優化,它完成使用調用者使用的型別無須進行轉換因此優化了運行時性能
  • 泛型函式如果執行很多具有不同型別參數的調用,則產生大量的機器代碼,最好不要再單個函式調用太多型別會給CPU緩存造成負擔

幾種流行的泛型語言的代價 Slow Compiler: c++/rust Slow Performance: java/scala Slow Programmer: go1

Rust-所有權(一)

所有權可以說是Rust核心概念,這讓Rust不需要垃圾回收(garbage collector)就可以保障記憶體安全。Rust的安全性和所有權的概念息息相,因此理解Rust中的所有權如何運作是非常重要的

所有權的規則

  • Rust 中每個數值都會有一個變數作為它的擁有者(owner)
  • 同時間只能有一個擁有者。
  • 當擁有者離開作用域時,數值就會被丟棄。

變數作用域

用下面這段程序描述變數範圍的概念

#![allow(unused)]
fn main() {
{
    // 在宣告以前,變數s無效
    let s = "hello";
    // 這裡是變數s的可用範圍
}
// 變數範圍已經結束,變數s無效
}

變數作用域是變數的一個屬性,其代表變數的可使用範圍,默認從宣告變數開始有效直到變數所在作用域結束。

記憶體與分配

定義一個變數並賦予值,這個變數的值存在記憶體中,例如需要用戶輸入的一串字串由於長度的不確定只能存放在堆(heap)上,這需要記憶體分配器在執行時請求記憶體並在不需要時還給分配器

在擁有垃圾回收機制(garbage collector, GC)的語言中,GC會追蹤並清除不再使用的記憶體,如果沒有GC的話則需要在不使用時顯式的呼叫釋放記憶體

例如C語言

#![allow(unused)]
fn main() {
{    
    char *s = strdup("hello");    
    free(s); *// 釋放s資源*
}
}

Rust選擇了一個不同的道路,當變數在離開作用域時會自動釋放例如下面

#![allow(unused)]
fn main() {
{
    let s = String::from("hello"); // s 在此開始視為有效
    // 使用 s
} // 此作用域結束,釋放s變數     
}

當變數離開作用域(大括號結束)時會自動呼叫特殊函示drop來釋放記憶體

變數與資料互動的方式

移動(Move)

變數可以在Rust中以不同的方式與相同的資料進行互動

#![allow(unused)]
fn main() {
let x = 100;
let y = x;
}

這個代碼將值100綁定到變數x,然後將x的值復制並賦值給變數y現在棧(stack)中將有兩個值100。此情況中的數據是"純量型別"的資料,不需要存儲到堆中,僅在棧(stack)中的資料的"移動"方式是直接複製,這不會花費更長的時間或更多的存儲空間。"純量型別"有這些:

  • 所有整數類型,例如 i32 、 u32 、 i64 等
  • 布爾類型 bool,值為true或false
  • 所有浮點類型,f32和f64
  • 字符類型 char
  • 僅包含以上類型數據的元組(Tuples)

現在來看一下非純量型別的移動

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1;
}

String物件的值"hello"為不固定長度長度型別所以被分配到堆(heap)

當s1賦值給s2,String的資料會被拷貝,不過我們拷貝是指標、長度和容量。我們不會拷貝指標指向的資料

前面説當變數超出作用域時,Rust自動調用釋放資源函數並清理該變數的記憶體。但是s1和s2都被釋放的話堆(heap)區中的"hello"被釋放兩次,這是不被系統允許的。為了確保安全,在給s2賦值時 s1已經無效了

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1; 
println!("{}, world!", s1); // 會發生錯誤 s1已經失效了
}

克隆(clone)

正常情況下Rust在較大資料上都會以淺拷貝的方式,當然也有提供深拷貝的method

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{} {}", s1, s2);

輸出
hello hello
}

Rust-所有權(二)

所有權與函式

將一個變數當作函式的參數傳給其他函式,怎樣安全的處理所有權

傳遞數值給函式這樣的語義和賦值給變數是類似的。傳遞變數給函式會是移動或拷貝就像賦值一樣

fn main() {
    // s被宣告
    let s = String::from("hello"); // s進入作用域

    takes_ownership(s); // s的值被當作參數傳入函式 所以可以當作s已經被移動,從這開始已經無效

    // x被宣告
    let x = 5; // x進入作用域

    makes_copy(x); // x的值被當作參數傳入函式,但x是純量型別 i32被copy,依然有效
} // 函式結束,x無效,接著是s的值已經被移動了它不會有任何動作

fn takes_ownership(some_string: String) {
    // 一個String參數some_string傳入,有效
    println!("{}", some_string);
} // 函式結束,參數some_string佔用的記憶體被釋放

fn makes_copy(some_integer: i32) {
    // 一個i32參數some_integer傳入,有效
    println!("{}", some_integer);
} // 函式結束,參數some_integer是純量型別,沒有任何動作發生

如果在呼叫takes_ownership之後在使用s變數在編譯時會出錯

回傳值與作用域

回傳值轉移所有權

fn main() {
    let s1 = gives_ownership(); // gives_ownership移動它的回傳值給s1
    let s2 = String::from("哈囉"); // s2進入作用域
    let s3 = takes_and_gives_back(s2); // s2移入takes_and_gives_back,該函式又將其回傳值移到s3
} // s3 在此離開作用域並釋放
  // s2 已被移走,所以沒有任何動作發生
  // s1 離開作用域並釋放

// 此函式回傳一個String
fn gives_ownership() -> String {
    let some_string = String::from("hello"); // some_string進入作用域

    return some_string; // 回傳some_string並移動給呼叫它的函式
}

// 此函式會取得一個String然後回傳它
fn takes_and_gives_back(a_string: String) -> String {
    // a_string進入作用域
    return a_string; // 回傳a_string並移動給呼叫的函式
}

引用與借用在前面介紹定義函式時有介紹過了,這邊就不多講了

講一下迷途指標(dangling pointer),這個在很多指標語言常發生的錯誤

簡單講就是用到空指標,Rust會在編譯時檢查這類型的錯誤

例如

#![allow(unused)]
fn main() {
fn dangle() -> &String { // 回傳String的迷途引用
    let s = String::from("hello"); // 宣告一個新的String
    return &s // 回傳String的引用
} // s在此會離開作用域並釋放
}

編譯時會產生錯誤 missing lifetime specifier

這個有關於生命週期的會在下一篇講

Rust-定義Closure(閉包)

一般來說Rust如果要排序數組會這樣寫

#![allow(unused)]
fn main() {
let mut arr = [10, 5, 9, 7, 6]

arr.sort();

println!("{:?}", arr);

輸出
[5, 6, 7, 9, 10]
}

剛剛升序如果要降序就必須使用sort_by 需要自己寫函式來傳給sort_by排序決定用

#![allow(unused)]
fn main() {
let mut arr = [10, 5, 9, 7, 6]

use std::cmp::Ordering;

fn descFn(a: &i32, b: &i32) -> Ordering {
    if  a < b {
        return Ordering::Greater;
    } else if a > b  {
        return Ordering::Less
    } else {
        return Ordering::Equal
    }
}

arr.sort_by(descFn);

println!("{:?}", arr);

輸出
[10, 9, 7, 6, 5]
}

造上面這樣使用有個缺點就是必須在宣告一個函式,如果只用一次的話就可用更簡短的使用匿名函式

#![allow(unused)]
fn main() {
let mut arr = [10, 5, 9, 7, 6]

use std::cmp::Ordering;

arr.sort_by(|a: &i32, b: &i32|
    if  a < b {
        return Ordering::Greater;
    } else if a > b  {
        return Ordering::Less
    } else {
        return Ordering::Equal
    }
);

println!("{:?}", arr);
輸出
[10, 9, 7, 6, 5]
}

使用一個匿名函式就可以簡短許多不用再宣告新的函式

Rust跟一般語言的匿名函式有個特別的限制就是無法使用外部宣告的變數

例如這樣是不合法的

#![allow(unused)]
fn main() {
let outVal = 10;

fn printOutVal() {
    println!("{}", outVal); // 無法訪問outVal變數
}

printOutVal();

// 編譯時會出現錯誤
}

但是使用靜態變數或是常量例如

#![allow(unused)]
fn main() {
const CONSTVAL: i32 = 10;

fn printOutVal() {
    println!("{}", CONSTVAL); // 合法使用
}

printOutVal();

-----------------------------

static STATICVAL: i32 = 0;

fn printOutVal() {
    println!("{}", CONSTVAL); // 合法使用
}

printOutVal();
}

Rust這樣限制應該基於所有權的設計關係,這樣有個好處就是避免亂引用變數造成BUG也比較好閱讀

當然Closure有更多複雜的使用這邊只簡單介紹最基本的使用方法

Rust-命令行的輸入輸出

命令行參數

一般來說編譯好的執行檔都是透過命令行來制執有些時候需要讀取一些命令行參數或是環境參數

程式輸入的最基本形式事示通命令行

例如下面

#![allow(unused)]
fn main() {
for arg in  std::env::args() {
    println!("{}", arg)
}
}

這時候編譯好的執行檔名稱如果是main在命令行執行

#![allow(unused)]
fn main() {
./main arg1 arg2 arg3

輸出
./main
arg1
arg2
arg3
}

args標準庫函式會返回命令行參數上的跌代器類別是Args並生成String值

第一個是程式名稱包含路徑其餘都是參數指令

環境變數

輸入輸出的另一種形式通過環境變數

#![allow(unused)]
fn main() {
for env in std::env::vars() {
    println!("{:?}", env)
}

輸出
("CARGO_HOME", "/playground/.cargo")
**("CARGO_MANIFEST_DIR", "/playground")
("CARGO_PKG_AUTHORS", "The Rust Playground")
("CARGO_PKG_DESCRIPTION", "")
("CARGO_PKG_HOMEPAGE", "")
("CARGO_PKG_LICENSE", "")
...**
}

剛剛是把每個環境變數都印出來假如要讀取特定環境變數可以透過var函示指定key

#![allow(unused)]
fn main() {
println!("{:?}", std::env::var("envkey"));
}

既然能讀取當然也能寫入,下面就透過set_var函式來寫入環境變數

#![allow(unused)]
fn main() {
std::env::set_var("setenvkey", "env_value");

println!("{:?}", std::env::var("setenvkey"));

輸出
Ok("env_value")
}

命令行的輸入

可以在程式啟動後獲取從鍵盤輸入的一行字直到用戶按下Enter鍵在輸出

#![allow(unused)]
fn main() {
let mut line = String::new();

std::io::stdin().read_line(&mnt line);

println!("{}", line);
}

Rust-特徵(Trait)(一)

什麼是特徵

根據官網的解釋就是

特徵會告訴編譯器特定型別與其他型別共享的功能。可以使用特徵定義來抽象出共同行為。可以使用特徵界限(trait bounds)來指定泛型型別為擁有特定行為的任意型別。

簡單講就是其他語言中的介面(interface),只是有少許不同而已

特徵的需求

假設我們需要一個函式需計算四次平方根,可以用標準庫sqrtx來寫

fn main() {
    fn quartic(x: f64) -> f64 {
        return x.sqrt().sqrt();
    }

    let qr = quartic(100.);

    println!("{} {}", qr * qr * qr * qr, qr);
}

輸出
100.00000000000003 3.1622776601683795

但假如要還需要一個計算32位元浮點數的四次平方根又要再寫一個quarticf32函式

#![allow(unused)]
fn main() {
fn quarticf64(x: f64) -> f64 {
    return x.sqrt().sqrt();
}

fn quarticf32(x: f32) -> f32 {
    return x.sqrt().sqrt();
}
}

這時候可能會寫一個泛型函式來取代前面兩個函式

fn main() {
    fn quartic<T>(x: T) -> T {
        return x.sqrt().sqrt();
    }

    let qrf32 = quartic(100f32);
	let qrf64 = quartic(100f64);

    println!("{} {}", qrf32, qrf64);
}

會出現編譯錯誤
no method named `sqrt` found for type parameter `T` in the current scope

會出現這樣的錯誤是因為x變數是屬於泛型型別T,是剛剛在建立出來的並沒有sqrt的mehtod

這時候就可以特徵派上用場的時候了,可以這樣使用解決剛剛的問題

fn main() {
    trait HasSqrt {
        fn sq(self) -> Self;
    }

    impl HasSqrt for f32 {
        fn sq(self) -> Self {
            return f32::sqrt(self);
        }
    }

    impl HasSqrt for f64 {
        fn sq(self) -> Self {
            return f64::sqrt(self);
        }
    }

    fn quartic<T>(x: T) -> T
    where
        T: HasSqrt,
    {
        return x.sq().sq();
    }

    let qr32 = quartic(100f32);
    let qr64 = quartic(100f64);

    println!("{} {}", qr32, qr64);
}

輸出
3.1622777 3.1622776601683795

一開先宣告HasSqrt trait然後使用impl關鍵字替f32和f64實現sq函式

Rust-特徵(Trait)(二)

沒有Trait界限的泛型函式

上一篇範例中在宣告泛型函式中使用了where

#![allow(unused)]
fn main() {
where
    T: HasSqrt,
{
}

在泛型函式的宣告中如果沒有where的子句引用類型參數,則該型別就沒有與任何trait關聯

因此只能對該泛型型別的物件做很少的事情

例如

#![allow(unused)]
fn main() {
fn f<T>(x: T) -> T {
    let c: T = x;
    let mut d = c;
    f = f2(d)
    return f
}
}

使用無界限的類型參數"T"只能

  • 通過值或引用將其作函式參數傳遞
  • 通過值或引用得從函式返回
  • 局部變數宣告或是初始化

多函式Trait

Trait也可以包含多個函式例如上一篇的範例如果要再增加絕對值abs的函式可以這樣寫

fn main() {
    trait HasSqrt {
        fn sq(self) -> Self;
        fn abs(self) -> Self;
    }

    impl HasSqrt for f32 {
        fn sq(self) -> Self {
            return f32::sqrt(self);
        }

        fn abs(self) -> Self {
            return f32::abs(self);
        }
    }

    impl HasSqrt for f64 {
        fn sq(self) -> Self {
            return f64::sqrt(self);
        }

        fn abs(self) -> Self {
            return f64::abs(self);
        }
    }

    fn quartic<T>(x: T) -> T
    where
        T: HasSqrt,
    {
        return x.abs.sq().sq();
    }

    let qr32 = quartic(100f32);
    let qr64 = quartic(100f64);

    println!("{} {}", qr32, qr64);
}

有時候可能只會需要平方根函式不需要絕對值函式這時候就可以抽兩個Trait會更靈活

fn main() {
    trait HasSqrt {
        fn sq(self) -> Self;
    }

    trait HasAbs {
        fn abs(self) -> Self;
    }

    impl HasSqrt for f32 {
        fn sq(self) -> Self {
            return f32::sqrt(self);
        }
    }

    impl HasAbs for f32 {
        fn abs(self) -> Self {
            return f32::abs(self);
        }
    } 

    impl HasSqrt for f64 {
        fn sq(self) -> Self {
            return f64::sqrt(self);
        }
    }

    impl HasAbs for f64 {
        fn abs(self) -> Self {
            return f64::abs(self);
        }
    }

    fn quartic<T>(x: T) -> T
    where
        T: HasSqrt + HasAbs , // 這裡界限宣告兩個trait
    {
        return x.abs.sq().sq();
    }

    let qr32 = quartic(100f32);
    let qr64 = quartic(100f64);

    println!("{} {}", qr32, qr64);
}

"self"和"Self"傻傻分不清楚

Rust式區分大小寫得

在前面的範例都使用到self和Self其中

  • 小寫開頭的self表示函式的值
  • 大寫開頭的Self標示self的型別

"self"和"Self"只能在trait或impl範圍裡面使用並且如果self必須是是方法的第一個參數

#![allow(unused)]
fn main() {
// 這幾個範例是等效的
fn f(self) -> Self
fn f(self: Self) -> Self
fn f(self: i32) -> Self
fn f(self) -> i32
fn f(self: Self) -> i32
fn f(self: i32) -> i32
}

預設實作

可以針對Trait預設行為,不必要求每個型別都要實作方法,當然也可以覆蓋這個預設的方法

fn main() {
    trait HasSqrt {
        fn sq(self) -> Self;
        fn helloWord(&self) -> String { // 預設實作
            return String::from("hello word");
        }
    }

    impl HasSqrt for f32 {
        fn sq(self) -> Self {
            return f32::sqrt(self);
        }
    }

    fn quartic<T>(x: T) -> T
    where
        T: HasSqrt,
    {
        println!("{}", x.helloWord());
        return x.sq().sq();
    }

    let qr32 = quartic(100f32);
}

Rust-並行&並發(一)

有關於並行和並發的定義每個人可能有不一樣的解釋

  • 並行指的是在同一時刻,多條指令在 CPU 上同時執行
  • 並發指的是在同一時間區間內,多條指令在 CPU 上同時執行

img

Rust以安全高效處理並行程式設計著稱

透過spawn建立新的執行緒

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("{}", i);
            thread::sleep(Duration::from_millis(1)); // 讓執行短暫sleep一下
        }
    });

    thread::sleep(Duration::from_millis(5)); // 讓執行短暫sleep一下
}

透過範例可以看到印出來並不是如預期的都會把1-10印出來,主要是因為main已經執行完退出造成新建的執行緒還沒跑完就被中斷了,雖然可以透過更久的sleep讓main晚點退出但是有更好的作法

使用join等待所有執行緒完成

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("{}", i);
            thread::sleep(Duration::from_millis(1)); // 讓執行短暫sleep一下
        }
    });

    handle.join().unwrap(); // (Blocking)讓全部執行完才繼續往下
}

透過儲存thread::spawn回傳的數值為變數,可以修正產生的執行緒完全沒有執行或沒有執行完成的問題。handle呼叫join方法來確保產生的執行緒會在main離開之前完成

透過執行緒使用move閉包

閉包透過move讓thread::spawn執行緒可以使用其他執行緒的資料。

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || { // 透過move讓執行緒可以擁有v變數的所有權
        println!("{:?}", v);
    }); // 在這個執行緒執行完之前v所有權都不會釋放

    handle.join().unwrap();
}

Rust-並行&並發(二)

channel

通常channel都是搭配並行使用,沒有使用並行就沒有使用channel的意義 「別透過共享記憶體來溝通,而是透過溝通來共享記憶體」。沒錯Golang的口號在Rust也是通用 Rust標準函式庫也有提供類似Golang的Channel函式 Channel函式會回傳兩個變數分別為發送者(transmitter)與接收者(receiver) Channel可以多個發送者(transmitter),單一接收者(receiver) 藉由這兩變數來傳遞訊息

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel(); // 建立新的channel,回傳一個元組,拆成兩個變數

    thread::spawn(move || {
        let val = String::from("hello word");
        tx.send(val).unwrap();
    });

    let received = rx.recv().unwrap(); // (Blocking)等到有資料才會繼續往下 
    println!("{}", received);
}

channel如果要在同一個通道多個執行緒使用發送者(transmitter)時,會因為所有權的關係造成錯誤,所以可以用clone來製造多個發送者(transmitter)

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();
    let tx1 = tx.clone(); // 複製發送者
    let tx2 = tx.clone(); // 複製發送者
    thread::spawn(move || {
        let val = String::from("hello word tx");
        tx.send(val).unwrap();
    });

    thread::spawn(move || {
        let val = String::from("hello word tx1");
        tx1.send(val).unwrap();
    });

    thread::spawn(move || {
        let val = String::from("hello word tx2");
        tx2.send(val).unwrap();
    });

    for received in rx {
        println!("{}", received);
    }
}

輸出
hello word tx
hello word tx2
hello word tx1

雖然channel本身不支援多個接收者(receiver),但是可以利用上鎖讓多個執行序同時使用接收者

錯誤處理

Rust將錯誤分成兩大類

  • 不可復原的(unrecoverable)
  • 可復原的(recoverable)

至於什麼時候該用什麼樣的錯誤就要看使用情境了

例如程式啟動時讀不到設定檔這個就可以使用不可復原的錯誤

不可復原的錯誤使用panic!

fn main() {
    panic!("恐慌性錯誤");
}

輸出
thread 'main' panicked at '恐慌性錯誤', main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

panic會印出錯誤訊息以及第幾行的訊息,還有提示可以設定RUST_BACKTRACE環境變數顯示錯誤回朔資訊

下面執行時增加環境變數RUST_BACKTRACE=1

#![allow(unused)]
fn main() {
RUST_BACKTRACE=1 ./main

輸出
thread 'main' panicked at '恐慌性錯誤', main.rs:2:5
stack backtrace:
   0: std::panicking::begin_panic
   1: main::main
   2: core::ops::function::FnOnce::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
}

這時候可以看到簡略的錯誤,也可以設定RUST_BACKTRACE=full會列出整個過程

Rust的panic不像Golang可以recover回來

可復原的的錯誤使用Result

Resut是個枚舉型別

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

Reust是枚舉型別就可以透過match來判斷是回傳OK還是回傳Err,再依裡面的泛型參數取值

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let _ = match f {
        Ok(file) => {
            println!("{:?}", file)
        }
        Err(error) => {
            println!("{:?}", error)
        }
    };
}

輸出
Os { code: 2, kind: NotFound, message: "No such file or directory" }

如果覺得match判斷錯誤太麻煩,也可以透過unwrap或expect來直接產生panic錯誤

#![allow(unused)]
fn main() {
let f = File::open("hello.txt").unwrap(); // 無法指定錯誤訊息
let f = File::open("hello.txt").expect("開啟hello.txt錯誤"); // 可以指定錯誤訊息
}

也可以在寫函式時回傳Result型別,這邊簡單定義Result泛型型別

fn main() {
    let ok = return_result_ok();
    println!("{:?}", ok);
    let error = return_result_error();
    println!("{:?}", error);
}

fn return_result_ok() -> Result<String, String> {
    let s = String::from("成功");
    return Ok(s);
}

fn return_result_error() -> Result<String, String> {
    let s = String::from("失敗");
    return Ok(s);
}

Rust-30天的心得

分享一下這30天從無到有的學習下來的一點點心得 先說一下為什麼要學習Rust是因為最近比較紅之外還有就是它滿常被拿來跟Golnag比較 身為一個Gopher當然要抱著他山之石可以攻玉心去了解一下Rust跟Golang的差別

Go和Rust有很多共同點。兩者都是比較近代的語言,兩者都是為了應對創建者在行業內現有語言中遇到的缺點而創建的,尤其是開發者生產力、可擴展性、安全性和並發性方面的缺點。

網路上搜尋"Rust vs. Go"滿滿的比較文這邊就不再講了 對我來說Rust優點有哪裡

  • 沒有GC
  • 零成本抽象
  • 記憶體控管
  • 安全性
  • 強大的編譯器錯誤提示

要說缺點就只有學習曲線比較陡,並不是很好上手 Rust用了複雜的方式解決複雜的問題,有時候解決的方式比問題更複雜

至於它有沒有比Golang好就不多做評論了, 但是隻聽說從Golang跳到Rust沒聽過從Rust跳到Golang 語言沒有好壞(不是南北拳的問題,是你的問題)

出處: https://ithelp.ithome.com.tw/users/20129675/ironman/4260

模組 (Module) 和套件 (Package)

我們學會函式後,程式碼可以分離,然而,隨著專案規模上升,函式名稱有可能相互衝突。雖然,我們也可以修改函式名稱,但是,只靠函數名稱來區分函式,往往會造成函數名稱變得冗長。像 C 語言中,沒有額外的機制處理函式名稱的衝突,就會看到很多長名稱的函式,像是 gtk_application_get_windows (出自 GTK+ 函式庫)。Rust 提供模組 (module) 的機制,處理函式命名衝突的問題。

註:在許多程式語言中,以命名空間 (namespace) 提供類似的機制。

在我們先前的內容中,函式和主程式都寫在同一個檔案。在實務上,我們會將函式或物件獨立出來,製成套件 (package),之後可以重覆利用。例如,同一套函式庫,可供終端機或圖形介面等不同使用者介面來使用。在 Rust 中,套件和模組相互關連。Rust 的套件又稱為 crate。

使用模組

雖然我們在先前的內容沒有強調模組,實際上,我們已經在使用模組了。我們回頭看先前的一個例子:

// Call f64 module
use std::f64;
 
fn main() {
    // Call sqrt function
    let n = f64::sqrt(4.0);
 
    println!("{}", n);
}

在這個例子中,我們呼叫 std 函式庫之中的 f64 模組,之後,就可以呼叫該模組內的 sqrt 函式。

撰寫模組

在 Rust 中,使用 mod 這個關鍵字來建立模組。如下例:

mod english {
    pub fn hello(name: &str) {
        println!("Hello, {}", name);
    }
}
 
mod chinese {
    pub fn hello(name: &str) {
        println!("你好,{}", name);
    }
}
 
fn main() {
    // Call modules
    use english;
    use chinese;
 
    // Call two functions with the same name
    english::hello("Michael");
    chinese::hello("麥可");
}

在本例中,我們在兩個模組中定義了同名而不同功能的函式。由於這兩個函式被區隔不同的模組中,不會有命名衝突的問題。模組除了區隔函式名稱外,也提供私有區塊,在模組中的函式或物件,需以 pub 關鍵字宣告,否則無法在模組外使用。

如同我們先前看到的範例,模組也可以內嵌。如下例:

mod english {
    pub mod greeting {
        pub fn hello(name: &str) {
            println!("Hello, {}", name);
        }
    }
 
    pub mod farewell {
        pub fn goodbye(name: &str) {
            println!("Goodbye, {}", name);
        }
    }
}
 
fn main() {
    use english;
 
    english::greeting::hello("Michael");
    english::farewell::goodbye("Michael");
}

由本例可知,透過模組的機制,可以協助我們整理函式。

建立套件

在我們先前的範例中,我們建立的是應用程式專案,如下:

cargo new --bin myapp

但若想將函式或物件獨立出來,供其他 Rust 程式使用,則要用函式庫專案,如下:

cargo new --lib mylib

我們現在實際建立一個函式庫套件。以上述指令建立 mylib 函式庫套件。加入以下函式:

#![allow(unused)]
fn main() {
// mylib/src/lib.rc
pub fn hello(name: &str) -> String {
    format!("Hello, {}", name)
}
}

之後,退回到上一層目錄,建立 myapp 主程式套件。加入以下內容:

// myapp/src/main.rs
 
// Call mylib
extern crate mylib;
 
fn main() {
    assert_eq!(mylib::hello("Michael"), "Hello, Michael");
}

透過 extern crate 可以呼叫外部專案。另外,要修改 Cargo.toml 紀錄檔,加入以下內容:

#![allow(unused)]
fn main() {
[dependencies]
mylib = { path = "../mylib" }
}

之後,執行該專案,若可正確執行,代表我們成功地建立套件。

如果函式庫存放在遠端站臺上,需修改存取位置。在下例中,我們存取以 Git 存放的函式庫:

#![allow(unused)]
fn main() {
[dependencies]
rand = { git = "https://github.com/rust-lang-nursery/rand.git" }
}

Cargo.toml 是 Rust 套件 (i.e. crate) 使用的設定檔。建議花一些時間熟悉其官方文件

在套件中使用模組

在我們先前的例子中,透過 mylib 函式庫對函式命名做最基本的區隔。不過,我們也可以在函式庫中使用模組來進一步區隔函式。我們先以實例看加入模組後的效果:

// Call external library
extern crate phrase;
 
fn main() {
    assert_eq!("Hello, Michael", phrase::english::greeting::hello("Michael"));
    assert_eq!("你好,麥可", phrase::chinese::greeting::hello("麥可"));
}

同樣地,需於 Cargo.toml 加入套件位置:

[dependencies]
phrase = { path = "../phrase" }

我們現在要實際建立這個函式庫。退回上一層目錄,建立 phrase 函式庫專案:

cargo new --lib phrase
$ tree
.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── chinese
    │   ├── greeting.rs
    │   └── mod.rs
    ├── english
    │   ├── greeting.rs
    │   └── mod.rs
    └── lib.rs

src/lib.rs 中宣告模組,記得要宣告公開權限:

src/english/mod.rs 中宣告子模組:

#![allow(unused)]
fn main() {
pub mod greeting;
}
#![allow(unused)]
fn main() {
pub fn hello(name: &str) -> String {
    format!("Hello, {}", name)
}
}

同樣地,在 src/chinese/mod.rs 中宣告子模組:

#![allow(unused)]
fn main() {
pub mod greeting;
}

同樣地,在 src/chinese/greeting.rs 中實作函式:

#![allow(unused)]
fn main() {
pub fn hello(name: &str) -> String {
	format!("你好,{}", name)
}
}

由於 Rust 的模組及套件和檔案名稱是連動的,若使用錯誤的檔案名稱將無法編譯,需注意。

進階的模組使用方式

在先前的例子中,由於函式庫結構較複雜,使得函式呼叫的動作變得繁瑣,Rust 提供別名來簡化這個動作。如下例:

extern crate phrase;
 
use phrase::english::greeting as en_greeting;
use phrase::chinese::greeting as zh_greeting;
 
fn main() {
    assert_eq!("Hello, Michael", en_greeting::hello("Michael"));
    assert_eq!("你好,麥可", zh_greeting::hello("麥可"));
}

Rust 官方文件中提供了另一個更複雜的模組呼叫範例:

// Rename crate
extern crate phrases as sayings;
 
// Rename module
use sayings::japanese::greetings as ja_greetings;
 
// Glob all functions in a module, NOT a good style
use sayings::japanese::farewells::*;
 
// A complex renaming scheme
use sayings::english::{self, greetings as en_greetings, farewells as en_farewells};
 
fn main() {
    println!("Hello in English; {}", en_greetings::hello());
    println!("And in Japanese: {}", ja_greetings::hello());
    println!("Goodbye in English: {}", english::farewells::goodbye());
    println!("Again: {}", en_farewells::goodbye());
 
    // Use a globbed function, AVOID it when possible.
    println!("And in Japanese: {}", goodbye());
}

稍微閱讀一下程式碼,大概就知道如何呼叫模組。要注意的是,globbing 的動作,會直接暴露函式名稱到主程式中,喪失使用模組區隔函式名稱的用意,應盡量避免。


Rust模組組織結構

出處 :https://jasonkayzk.github.io/2022/11/19/Rust%E6%A8%A1%E5%9D%97%E7%BB%84%E7%BB%87%E7%BB%93%E6%9E%84/

  1. 基本說明

  2. Package、Crate和Module

    1. 包 Crate
    2. 項目 Package
      1. 二進制 Package
      2. 庫 Package
      3. 易混淆的 Package 和包
      4. 典型的 Package 結構
    3. 模組 Module
      1. 建立巢狀模組
      2. 模組樹
        1. 父子模組
      3. 用路徑引用模組
        1. 絕對路徑引用
        2. 相對路徑引用
        3. 絕對還是相對?
      4. 程式碼可見性
      5. 使用 super 引用模組
      6. 使用 self 引用模組
      7. 結構體和列舉的可見性
      8. 模組與檔案分離
  3. 使用 use 及受限可見性

    1. 基本引入方式
      1. 絕對路徑引入模組
      2. 相對路徑引入模組中的函數
      3. 引入模組還是函數?
    2. 避免同名引用
      1. 模組::函數
      2. as 別名引用
    3. 引入項再匯出
  4. 使用第三方包

  5. 使用 {} 簡化引入方式

  6. 使用 * 引入模組下的所有項

  7. 其他引入模組的方式

  8. 受限的可見性

    1. 限制可見性語法
  9. 一個單檔案多模組的使用案例

  10. 多個目錄間模組引用

  11. 多個Cargo之間進行引用

本文講述了Rust中模組的組織形式和約定;

原始碼:

  • https://github.com/JasonkayZK/rust-learn/tree/project-structure

Rust模組組織結構

基本說明

當工程規模變大時,把程式碼寫到一個甚至幾個檔案中,都是不太聰明的做法,可能存在以下問題:

  1. 單個檔案過大,導致打開、翻頁速度大幅變慢
  2. 查詢和定位效率大幅降低,類比下,你會把所有知識內容放在一個幾十萬字的文件中嗎?
  3. 只有一個程式碼層次:函數,難以維護和協作,想像一下你的作業系統只有一個根目錄,剩下的都是單層子目錄會如何:disaster

同時,將大的程式碼檔案拆分成包和模組,還允許我們實現程式碼抽象和復用:將你的程式碼封裝好後提供給使用者,那麼使用者只需要呼叫公共介面即可,無需知道內部該如何實現;

Rust 有自己的規則和約定來組織其模組;例如:一個 crate 包最多可以有一個庫 crate,任意多個二進制crate、匯入資料夾內的模組的兩種約定方式等等;

先把一些術語說明一下:

  • 項目(Packages):一個 Cargo 提供的 feature,可以用來建構、測試和分享包;
  • 包(Crate):一個由多個模組組成的樹形結構,可以作為三方庫進行分發,也可以生成可執行檔案進行運行;
  • 模組(Module):可以一個檔案多個模組,也可以一個檔案一個模組,模組可以被認為是真實項目中的程式碼組織單元;

首先,包(crate) 是 Cargo 中的定義,執行 cargo new xxxx 就是建立了一個包,crate 是二進制(bin)或庫(lib)項目;

Rust 約定:在 Cargo.toml 的同級目錄下:

  • 包含src/main.rs檔案,就是與包同名的二進制crate
  • 包含src/lib.rs,就是與包同名的庫crate

一個包內可以有多個 crate,多個crates就是一個模組的樹形結構;例如,如果一個包內同時包含src/main.rssrc/lib.rs,那麼他就有兩個crate

如果想要包含多個二進制craterust規定:需要將檔案放在src/bin目錄下,每個檔案就是一個單獨的crate

crate root 是用來描述如何建構crate的檔案;例如:src/main.rssrc/lib.rs 都是crate root

crate root將由Cargo傳遞給rustc來實際建構庫或者二進制項目!

這也是為什麼,入口檔案中要寫入各個模組:mod xxx; 才能使其生效!

帶有 Cargo.toml 檔案的包用來整體描述如何建構crate;同時,一個包可以最多有一個庫crate,任意多個二進制crate

Package、Crate和Module

項目 Package 和包 Crate 的概念很容易被搞混,甚至在很多書中,這兩者都是不分的,但是由於官方對此做了明確的區分,因此我們會在本章節中試圖(掙紮著)理清這個概念;

包 Crate

對於 Rust 而言,crate 是一個獨立的可編譯單元,它編譯後會生成一個可執行檔案或者一個庫;

一個包會將相關聯的功能打包在一起,使得該功能可以很方便的在多個項目中分享;

例如:標準庫中沒有提供、而是在三方庫中提供的 rand 包;它提供了隨機數生成的功能,我們只需要將該包通過 use rand; 引入到當前項目的範疇中,就可以在項目中使用 rand 的功能:rand::XXX

**同一個包中不能有同名的類型,但是在不同包中就可以;**例如,雖然 rand 包中,有一個 Rng 特徵,可是我們依然可以在自己的項目中定義一個 Rng,前者通過 rand::Rng 訪問,後者通過 Rng 訪問,對於編譯器而言,這兩者的邊界非常清晰,不會存在引用歧義;

項目 Package

鑑於 Rust 團隊標新立異的起名傳統,以及包的名稱被 crate 佔用,庫的名稱被 library 佔用,經過斟酌, 我們決定將 Package 翻譯成項目,你也可以理解為工程、軟體包;

由於 Package 就是一個項目,因此它包含有獨立的 Cargo.toml 檔案,以及因為功能性被組織在一起的一個或多個包;一個 Package 只能包含一個庫(library)類型的包,但是可以包含多個二進制可執行類型的包;

二進制 Package

下面的命令可以建立一個二進制 Package

$ cargo new my-project
     Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs

這裡,Cargo 為我們建立了一個名稱是 my-projectPackage,同時在其中建立了 Cargo.toml 檔案,可以看一下該檔案,裡面並沒有提到 src/main.rs 作為程序的入口;

原因是 Cargo 有一個慣例:src/main.rs 是二進制包的根檔案,該二進制包的包名跟所屬 Package 相同,在這裡都是 my-project,所有的程式碼執行都從該檔案中的 fn main() 函數開始;

使用 cargo run 可以運行該項目,輸出:Hello, world!

庫 Package

再來建立一個庫類型的 Package

$ cargo new my-lib --lib
     Created library `my-lib` package
$ ls my-lib
Cargo.toml
src
$ ls my-lib/src
lib.rs

首先,如果你試圖運行 my-lib,會報錯:

$ cargo run
error: a bin target must be available for `cargo run`

原因是:庫類型的 Package 只能作為三方庫被其它項目引用,而不能獨立運行,只有之前的二進制 Package 才可以運行;

src/main.rs 一樣,Cargo 知道,如果一個 Package 包含有 src/lib.rs,意味它包含有一個庫類型的同名包 my-lib,該包的根檔案是 src/lib.rs

易混淆的 Package 和包

看完上面,相信大家看出來為何 Package 和包容易被混淆了吧?因為你用 cargo new 建立的 Package 和它其中包含的包是同名的!

不過,只要你牢記:Package 是一個項目工程,而包只是一個編譯單元,基本上也就不會混淆這個兩個概念了:src/main.rssrc/lib.rs 都是編譯單元,因此它們都是包;

典型的 Package 結構

上面建立的 Package 中僅包含 src/main.rs 檔案,意味著它僅包含一個二進制同名包 my-project

如果一個 Package 同時擁有 src/main.rssrc/lib.rs,那就意味著它包含兩個包:庫包和二進制包;

同時,這兩個包名也都是 my-project —— 都與 Package 同名;

一個真實項目中典型的 Package,會包含多個二進制包,這些包檔案被放在 src/bin 目錄下,每一個檔案都是獨立的二進制包,同時也會包含一個庫包,該包只能存在一個 src/lib.rs

.
├── Cargo.toml
├── Cargo.lock
├── src
│   ├── main.rs
│   ├── lib.rs
│   └── bin
│       └── main1.rs
│       └── main2.rs
├── tests
│   └── some_integration_tests.rs
├── benches
│   └── simple_bench.rs
└── examples
    └── simple_example.rs
  • 唯一庫包:src/lib.rs
  • 默認二進制包:src/main.rs,編譯後生成的可執行檔案與 Package 同名
  • 其餘二進制包:src/bin/main1.rssrc/bin/main2.rs,它們會分別生成一個檔案同名的二進制可執行檔案
  • 整合測試檔案:tests 目錄下
  • 基準性能測試 benchmark 檔案:benches 目錄下
  • 項目示例:examples 目錄下

這種目錄結構基本上是 Rust 的標準目錄結構,在 GitHub 的大多數項目上,你都將看到它的身影;

理解了包的概念,我們再來看看構成包的基本單元:模組;

模組 Module

本小節講深入講解 Rust 的程式碼構成單元:模組;

使用模組可以將包中的程式碼按照功能性進行重組,最終實現更好的可讀性及易用性;

同時,我們還能非常靈活地去控制程式碼的可見性,進一步強化 Rust 的安全性;

建立巢狀模組

小餐館,相信大家都挺熟悉的,學校外的估計也沒少去,那麼咱就用小餐館為例,來看看 Rust 的模組該如何使用;

可以使用 cargo new --lib restaurant 建立一個小餐館;

注意,這裡建立的是一個庫類型的 Package,然後將以下程式碼放入 src/lib.rs 中:

#![allow(unused)]
fn main() {
// 餐廳前廳,用於吃飯
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}
}

以上的程式碼(在同一個檔案中就)建立了三個模組,有幾點需要注意的:

  • 使用 mod 關鍵字來建立新模組,後面緊跟著模組名稱;
  • 模組可以巢狀,這裡巢狀的原因是招待客人和服務都發生在前廳,因此我們的程式碼模擬了真實場景;
  • 模組中可以定義各種 Rust 類型,例如函數、結構體、列舉、特徵等;
  • 所有模組均定義在同一個檔案中;

類似上述程式碼中所做的,使用模組,我們就能將功能相關的程式碼組織到一起,然後通過一個模組名稱來說明這些程式碼為何被組織在一起,這樣其它程式設計師在使用你的模組時,就可以更快地理解和上手;

模組樹

之前我們提到過 src/main.rssrc/lib.rs 被稱為包根(crate root),是由於這兩個檔案的內容形成了一個模組 crate,該模組位於包的樹形結構(由模組組成的樹形結構)的根部:

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

這顆樹展示了模組之間彼此的巢狀關係,因此被稱為模組樹

其中 crate 包根是 src/lib.rs 檔案,包根檔案中的三個模組分別形成了模組樹的剩餘部分;

父子模組

如果模組 A 包含模組 B,那麼 AB 的父模組,BA 的子模組;

在上例中,front_of_househostingserving 的父模組,反之,後兩者是前者的子模組;

聰明的讀者,應該能聯想到,模組樹跟電腦上檔案系統目錄樹的相似之處;

然而不僅僅是組織結構上的相似,就連使用方式都很相似:每個檔案都有自己的路徑,使用者可以通過這些路徑使用它們,在 Rust 中,我們也通過路徑的方式來引用模組;

用路徑引用模組

想要呼叫一個函數,就需要知道它的路徑,在 Rust 中,這種路徑有兩種形式:

  • 絕對路徑,從包根開始,路徑名以包名或者 crate 作為開頭
  • 相對路徑,從當前模組開始,以 selfsuper 或當前模組的識別碼作為開頭

讓我們繼續經營那個慘淡的小餐館,這次為它實現一個小功能:

src/lib.rs

#![allow(unused)]
fn main() {
// 餐廳前廳,用於吃飯
pub mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 絕對路徑
    crate::front_of_house::hosting::add_to_waitlist();

    // 相對路徑
    front_of_house::hosting::add_to_waitlist();
}
}

eat_at_restaurant 是一個定義在 crate root 中的函數,在該函數中使用了兩種方式對 add_to_waitlist 進行呼叫;

絕對路徑引用

因為 eat_at_restaurantadd_to_waitlist 都定義在一個包中,因此在絕對路徑引用時,可以直接以 crate 開頭,然後逐層引用,每一層之間使用 :: 分隔:

crate::front_of_house::hosting::add_to_waitlist();

對比下之前的模組樹:

crate
 └── eat_at_restaurant
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

可以看出,絕對路徑的呼叫,完全符合了模組樹的層級遞進,非常符合直覺;

如果類比檔案系統,就跟使用絕對路徑呼叫可執行程序差不多:/front_of_house/hosting/add_to_waitlist,使用 crate 作為開始就和使用 / 作為開始一樣;

相對路徑引用

再回到模組樹中,因為 eat_at_restaurantfront_of_house 都處於 crate root 中,因此相對路徑可以使用 front_of_house 作為開頭:

front_of_house::hosting::add_to_waitlist();

如果類比檔案系統,那麼它類似於呼叫同一個目錄下的程序,你可以這麼做:front_of_house/hosting/add_to_waitlist

絕對還是相對?

如果只是為了引用到指定模組中的對象,那麼兩種都可以;

但是在實際使用時,需要遵循一個原則:當程式碼被挪動位置時,儘量減少引用路徑的修改,相信大家都遇到過,修改了某處程式碼,導致所有路徑都要挨個替換,這顯然不是好的路徑選擇;

回到之前的例子:

如果我們把 front_of_house 模組和 eat_at_restaurant 移動到一個模組中 customer_experience,那麼絕對路徑的引用方式就必須進行修改:crate::customer_experience::front_of_house ...

但是假設我們使用的相對路徑,那麼該路徑就無需修改,因為它們兩個的相對位置其實沒有變:

crate
 └── customer_experience
    └── eat_at_restaurant
    └── front_of_house
        ├── hosting
        │   ├── add_to_waitlist
        │   └── seat_at_table

從新的模組樹中可以很清晰的看出這一點;

再比如,其它的都不動,把 eat_at_restaurant 移動到模組 dining 中,如果使用相對路徑,你需要修改該路徑,但如果使用的是絕對路徑,就無需修改:

crate
 └── dining
     └── eat_at_restaurant
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist

不過,如果不確定哪個好,你可以考慮優先使用絕對路徑,因為呼叫的地方和定義的地方往往是分離的,而定義的地方較少會變動;

程式碼可見性

Rust 出於安全的考慮,默認情況下,所有的類型都是私有化的,包括函數、方法、結構體、列舉、常數,是的,就連模組本身也是私有化的;

在 Rust 中,父模組完全無法訪問子模組中的私有項,但是子模組卻可以訪問父模組、父父..模組的私有項!

例如下面的程式碼是無法編譯通過的:

#![allow(unused)]
fn main() {
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 絕對路徑
    crate::front_of_house::hosting::add_to_waitlist();

    // 相對路徑
    front_of_house::hosting::add_to_waitlist();
}
}

hosting 模組是私有的,無法在包根進行訪問;

那麼為何 front_of_house 模組就可以訪問?

因為它和 eat_at_restaurant 同屬於一個包根範疇內,同一個模組內的程式碼自然不存在私有化問題(所以我們之前章節的程式碼都沒有報過這個錯誤!);

類似其它語言的 public 或者 Go 語言中的首字母大寫,Rust 提供了 pub 關鍵字,通過它你可以控制模組和模組中指定項的可見性;

使用 super 引用模組

在上文用路徑引用模組小節,使用路徑引用模組中,我們提到了相對路徑有三種方式開始:selfsupercrate 或者模組名,其中第三種在前面已經講到過,現在來看看通過 super 的方式引用模組項;

super 代表的是父模組為開始的引用方式,非常類似於檔案系統中的 ..

語法:../a/b 檔案名稱:

src/lib.rs

#![allow(unused)]
fn main() {
// 餐廳前廳,用於吃飯
pub mod front_of_house {
    pub mod serving {
        fn serve_order() {}

        // 廚房模組
        mod back_of_house {
            fn fix_incorrect_order() {
                cook_order();
                super::serve_order();
            }

            fn cook_order() {}
        }
    }
}
}

在廚房模組中,使用 super::serve_order 語法,呼叫了父模組中的 serve_order 函數;

那麼你可能會問,為何不使用 crate::serve_order 的方式?

其實也可以,不過如果你確定未來這種層級關係不會改變,那麼 super::serve_order 的方式會更穩定,未來就算它們都不在 crate root了,依然無需修改引用路徑;

所以路徑的選用,往往還是取決於場景,以及未來程式碼的可能走向;

使用 self 引用模組

self 其實就是引用自身模組中的項,也就是說和我們之前章節的程式碼類似,都呼叫同一模組中的內容,區別在於之前章節中直接通過名稱呼叫即可,而 self,你得多此一舉:

#![allow(unused)]
fn main() {
pub mod serving {

  fn serve_order() {
    self::back_of_house::cook_order()
  }

  // 廚房模組
  mod back_of_house {
    pub fn cook_order() {}
  }
}
}

是的,多此一舉,因為完全可以直接呼叫 back_of_house,但是 self 還有一個大用處,在後文中會講;

結構體和列舉的可見性

為何要把結構體和列舉的可見性單獨拎出來講呢?因為這兩個傢伙的成員欄位擁有完全不同的可見性:

  • 將結構體設定為 pub,但它的所有欄位依然是私有的;
  • 將列舉設定為 pub,它的所有欄位則將對外可見;

原因在於:列舉和結構體的使用方式不一樣:

  • 如果列舉的成員對外不可見,那該列舉將一點用都沒有,因此列舉成員的可見性自動跟列舉可見性保持一致,這樣可以簡化使用者的使用;
  • 而結構體的應用場景比較複雜,其中的欄位也往往部分在 A 處被使用,部分在 B 處被使用,因此無法確定成員的可見性,那索性就設定為全部不可見,將選擇權交給程式設計師;

模組與檔案分離

在之前的例子中,我們所有的模組都定義在 src/lib.rs 中,但是當模組變多或者變大時,需要將模組放入一個單獨的檔案中,讓程式碼更好維護;

現在,把 front_of_house 前廳分離出來,放入一個單獨的檔案中:

src/front_of_house.rs

#![allow(unused)]
fn main() {
// 餐廳前廳,用於吃飯
pub mod hosting {
    pub fn add_to_waitlist() {}

    fn seat_at_table() {}
}

pub mod serving {
    fn take_order() {}

    fn serve_order() {
        self::back_of_house::cook_order()
    }

    fn take_payment() {}

    // 廚房模組
    mod back_of_house {
        fn fix_incorrect_order() {
            cook_order();
            super::serve_order();
        }

        pub fn cook_order() {}
    }
}
}

然後,將以下程式碼留在 src/lib.rs 中:

#![allow(unused)]
fn main() {
mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    // 絕對路徑
    hosting::add_to_waitlist();

    // 相對路徑
    hosting::add_to_waitlist();
}
}

其實跟之前在同一個檔案中也沒有太大的不同,但是有幾點值得注意:

  • mod front_of_house:告訴 Rust 從另一個和模組 front_of_house 同名的檔案中載入該模組的內容;
  • 使用絕對路徑的方式來引用 hosting 模組:crate::front_of_house::hosting

需要注意的是,和之前程式碼中 mod front_of_house{..} 的完整模組不同:

現在的程式碼中,模組的聲明和實現是分離的,實現是在單獨的 front_of_house.rs 檔案中,然後通過 mod front_of_house; 這條聲明語句從該檔案中把模組內容載入進來;

因此我們可以認為:模組 front_of_house 的定義還是在 src/lib.rs 中,只不過模組的具體內容被移動到了 src/front_of_house.rs 檔案中;

在這裡出現了一個新的關鍵字 use,聯想到其它章節我們見過的標準庫引入 use std::fmt;,可以大致猜測,該關鍵字用來將外部模組中的項引入到當前範疇中來,這樣無需冗長的父模組前綴即可呼叫hosting::add_to_waitlist();,在下節中,我們將對 use 進行詳細的講解;

使用 use 及受限可見性

如果程式碼中,通篇都是 crate::front_of_house::hosting::add_to_waitlist 這樣的函數呼叫形式,我不知道有誰會喜歡;

因此我們需要一個辦法來簡化這種使用方式,在 Rust 中,可以使用 use 關鍵字把路徑提前引入到當前範疇中,隨後的呼叫就可以省略該路徑,極大地簡化了程式碼;

基本引入方式

在 Rust 中,引入模組中的項有兩種方式:絕對路徑和相對路徑,這兩者在前文中都講過,就不再贅述;

先來看看使用絕對路徑的引入方式;

絕對路徑引入模組

#![allow(unused)]
fn main() {
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}
}

這裡,我們使用 use 和絕對路徑的方式,將 hosting 模組引入到當前範疇中,然後只需通過 hosting::add_to_waitlist 的方式,即可呼叫目標模組中的函數;

相比 crate::front_of_house::hosting::add_to_waitlist() 的方式要簡單的多;

那麼,還能更簡單嗎?

相對路徑引入模組中的函數

在下面程式碼中,我們不僅要使用相對路徑進行引入,而且與上面引入 hosting 模組不同,直接引入該模組中的 add_to_waitlist 函數:

#![allow(unused)]
fn main() {
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
    add_to_waitlist();
    add_to_waitlist();
    add_to_waitlist();
}
}

很明顯,函數呼叫又變得更短了;

引入模組還是函數?

從使用簡潔性來說,引入函數自然是更甚一籌,但是在某些時候,引入模組會更好:

  • 需要引入同一個模組的多個函數
  • 範疇中存在同名函數

在以上兩種情況中,使用 use front_of_house::hosting 引入模組要比 use front_of_house::hosting::add_to_waitlist; 引入函數更好;

例如,如果想使用 HashMap,那麼直接引入該結構體是比引入模組更好的選擇,因為在 collections 模組中,我們只需要使用一個 HashMap 結構體:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}

其實嚴格來說,對於引用方式並沒有需要遵守的慣例,主要還是取決於你的喜好,不過我們建議:

優先使用最細粒度(引入函數、結構體等)的引用方式,如果引起了某種麻煩(例如前面兩種情況),再使用引入模組的方式;

避免同名引用

根據上一章節的內容,我們只要保證同一個模組中不存在同名項就行;

話雖如此,一起看看,如果遇到同名的情況該如何處理;

模組::函數

#![allow(unused)]
fn main() {
use std::fmt;
use std::io;

fn function1() -> fmt::Result {
    // --snip--
}

fn function2() -> io::Result<()> {
    // --snip--
}
}

rust上面的例子給出了很好的解決方案,使用模組引入的方式,具體的 Result 通過 模組::Result 的方式進行呼叫;

可以看出,避免同名衝突的關鍵,就是使用父模組的方式來呼叫;

除此之外,還可以給予引入的項起一個別名;

as 別名引用

對於同名衝突問題,還可以使用 as 關鍵字來解決,它可以賦予引入項一個全新的名稱:

#![allow(unused)]
fn main() {
use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --snip--
}

fn function2() -> IoResult<()> {
    // --snip--
}
}

如上所示,首先通過 use std::io::ResultResult 引入到範疇,然後使用 as 給予它一個全新的名稱 IoResult,這樣就不會再產生衝突:

  • Result 代表 std::fmt::Result;
  • IoResult 代表 std:io::Result

引入項再匯出

當外部的模組項 A 被引入到當前模組中時,它的可見性自動被設定為私有的,如果你希望允許其它外部程式碼引用我們的模組項 A,那麼可以對它進行再匯出:

#![allow(unused)]
fn main() {
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}
}

如上,使用 pub use 即可實現:

這裡 use 代表引入 hosting 模組到當前範疇,pub 表示將該引入的內容再度設定為可見;

當你希望將內部的實現細節隱藏起來或者按照某個目的組織程式碼時,可以使用 pub use 再匯出;

例如,統一使用一個模組來提供對外的 API,那該模組就可以引入其它模組中的 API,然後進行再匯出,最終對於使用者來說,所有的 API 都是由一個模組統一提供的;

使用第三方包

之前我們一直在引入標準庫模組或者自訂模組,現在來引入下第三方包中的模組;

關於如何引入外部依賴,在 Cargo 入門中就有講,這裡直接給出操作步驟:

  1. 修改 Cargo.toml 檔案,在 [dependencies] 區域新增一行:rand = "0.8.3"
  2. 此時,如果你用的是 VSCoderust-analyzer 外掛,該外掛會自動拉取該庫,你可能需要等它完成後,再進行下一步(VSCode 左下角有提示)

好了,此時,rand 包已經被我們新增到依賴中,下一步就是在程式碼中使用:

use rand::Rng;

fn main() {
    let secret_number = rand::thread_rng().gen_range(1..101);
}

這裡使用 use 引入了第三方包 rand 中的 Rng 特徵,因為我們需要呼叫的 gen_range 方法定義在該特徵中;

crates.io,lib.rs

Rust 社區已經為我們貢獻了大量高品質的第三方包,你可以在 crates.io 或者 lib.rs 中檢索和使用;

從目前來說尋找包更推薦 lib.rs,搜尋功能更強大,內容展示也更加合理,但是下載依賴包還是得用crates.io

使用 {} 簡化引入方式

對於以下一行一行的引入方式:

#![allow(unused)]
fn main() {
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::collections::HashSet;

use std::cmp::Ordering;
use std::io;
}

可以使用 {} 來一起引入進來,在大型項目中,使用這種方式來引入,可以減少大量 use 的使用:

#![allow(unused)]
fn main() {
use std::collections::{HashMap,BTreeMap,HashSet};
use std::{cmp::Ordering, io};
}

對於下面的同時引入模組和模組中的項:

#![allow(unused)]
fn main() {
use std::io;
use std::io::Write;
}

可以使用 {} 的方式進行簡化:

#![allow(unused)]
fn main() {
use std::io::{self, Write};
}

self

上面使用到了模組章節提到的 self 關鍵字,用來替代模組自身,結合上一節中的 self,可以得出它在模組中的兩個用途:

  • use self::xxx,表示載入當前模組中的 xxx。此時 self 可省略
  • use xxx::{self, yyy},表示,載入當前路徑下模組 xxx 本身,以及模組 xxx 下的 yyy

使用 \* 引入模組下的所有項

對於之前一行一行引入 std::collections 的方式,我們還可以使用

use std::collections::*;

以上這種方式來引入 std::collections 模組下的所有公共項,這些公共項自然包含了 HashMapHashSet 等想手動引入的集合類型;

使用 \* 來引入的時候要格外小心,因為你很難知道到底哪些被引入到了當前範疇中,有哪些會和你自己程序中的名稱相衝突:

use std::collections::*;

struct HashMap;
fn main() {
   let mut v =  HashMap::new();
   v.insert("a", 1);
}

以上程式碼中,std::collection::HashMap* 引入到當前範疇,但是由於存在另一個同名的結構體,因此 HashMap::new 根本不存在,因為對於編譯器來說,本地同名類型的優先順序更高;

在實際項目中,這種引用方式往往用於快速寫測試程式碼,它可以把所有東西一次性引入到 tests 模組中;

其他引入模組的方式

通過 #[path ="你的路徑"] 可以放在任何目錄都行,如:

#[path ="你的路徑"]
mod core;

可以無視 mod.rs 或者目錄方式:

image

當然,也可以在目錄下建立 mod.rs 檔案,但是需要一層一層的 pub mod 匯出,或者採用 2018 版本的模組目錄和模組.rs 同名方式(官方推薦),總之,#[path] 方式最靈活(慎用);

三種方式對比:

Rust 模組引用三種方式:

Rust 2015Rust 2018#[path = “路徑”]
. ├── lib.rs └── foo/ ├── mod.rs └── bar.rs. ├── lib.rs ├── foo.rs └── foo/ └── bar.rs. ├── lib.rs └── pkg/ // 任意目錄名 ├── foo.rs // #[path = “./pkg/foo.rs”] └── bar.rs // #[path = “./pkg/bar.rs”]

受限的可見性

在上一節中,我們學習了可見性這個概念,這也是模組體系中最為核心的概念,控制了模組中哪些內容可以被外部看見,但是在實際使用時,光被外面看到還不行,我們還想控制哪些人能看,這就是 Rust 提供的受限可見性;

例如,在 Rust 中,包是一個模組樹,我們可以通過 pub(crate) item; 這種方式來實現:item 雖然是對外可見的,但是隻在當前包內可見,外部包無法引用到該 item

所以,如果我們想要讓某一項可以在整個包中都可以被使用,那麼有兩種辦法:

  • 在crate root中定義一個非 pub 類型的 X(父模組的項對子模組都是可見的,因此包根中的項對模組樹上的所有模組都可見);
  • 在子模組中定義一個 pub 類型的 Y,同時通過 use 將其引入到包根;

例如:

#![allow(unused)]
fn main() {
mod a {
    pub mod b {
        pub fn c() {          
            println!("{:?}",crate::X);
        }

      // 在子模組中定義一個 `pub` 類型的 `Y`,同時通過 `use` 將其引入到包根
        #[derive(Debug)]
        pub struct Y;
    }
}

// 在crate root中定義一個非 `pub` 類型的 `X`(父模組的項對子模組都是可見的,因此包根中的項對模組樹上的所有模組都可見)
#[derive(Debug)]
struct X;
use a::b::Y;
fn d() {
    println!("{:?}",Y);
}
}

以上程式碼充分說明瞭之前兩種辦法的使用方式,但是有時我們會遇到這兩種方法都不太好用的時候;

例如希望對於某些特定的模組可見,但是對於其他模組又不可見:

#![allow(unused)]
fn main() {
// 目標:`a` 匯出 `I`、`bar` and `foo`,其他的不匯出
pub mod a {
    pub const I: i32 = 3;

    fn semisecret(x: i32) -> i32 {
        use self::b::c::J;
        x + J
    }

    pub fn bar(z: i32) -> i32 {
        semisecret(I) * z
    }
    pub fn foo(y: i32) -> i32 {
        semisecret(I) + y
    }

    mod b {
        mod c {
            const J: i32 = 4;
        }
    }
}
}

這段程式碼會報錯,因為與父模組中的項對子模組可見相反,子模組中的項對父模組是不可見的;

這裡 semisecret 方法中,a -> b -> c 形成了父子模組鏈,那 c 中的 J 自然對 a 模組不可見;

如果使用之前的可見性方式,那麼想保持 J 私有,同時讓 a 繼續使用 semisecret 函數的辦法是:將該函數移動到 c 模組中,然後用 pub usesemisecret 函數進行再匯出:

#![allow(unused)]
fn main() {
pub mod a {
    pub const I: i32 = 3;

    use self::b::semisecret;

    pub fn bar(z: i32) -> i32 {
        semisecret(I) * z
    }
    pub fn foo(y: i32) -> i32 {
        semisecret(I) + y
    }

    mod b {
        pub use self::c::semisecret;
        mod c {
            const J: i32 = 4;
            pub fn semisecret(x: i32) -> i32 {
                x + J
            }
        }
    }
}
}

這段程式碼說實話問題不大,但是有些破壞了我們之前的邏輯;

如果想保持程式碼邏輯,同時又只讓 Ja 內可見該怎麼辦?

#![allow(unused)]
fn main() {
pub mod a {
    pub const I: i32 = 3;

    fn semisecret(x: i32) -> i32 {
        use self::b::c::J;
        x + J
    }

    pub fn bar(z: i32) -> i32 {
        semisecret(I) * z
    }
    pub fn foo(y: i32) -> i32 {
        semisecret(I) + y
    }

    mod b {
        pub(in crate::a) mod c {
            pub(in crate::a) const J: i32 = 4;
        }
    }
}
}

通過 pub(in crate::a) 的方式,我們指定了模組 c 和常數 J 的可見範圍都只是 a 模組中,a 之外的模組是完全訪問不到它們的!

限制可見性語法

pub(crate)pub(in crate::a) 就是限制可見性語法,前者是限制在整個包內可見,後者是通過絕對路徑,限制在包內的某個模組內可見,總結一下:

  • pub 意味著可見性無任何限制;
  • pub(crate) 表示在當前包可見;
  • pub(self) 在當前模組可見;
  • pub(super) 在父模組可見;
  • pub(in <path>) 表示在某個路徑代表的模組中可見,其中 path 必須是父模組或者祖先模組;

一個單檔案多模組的使用案例

下面是一個模組的綜合例子:

my_mod/src/lib.rs

// 一個名為 `my_mod` 的模組
mod my_mod {
    // 模組中的項默認具有私有的可見性
    fn private_function() {
        println!("called `my_mod::private_function()`");
    }

    // 使用 `pub` 修飾語來改變默認可見性。
    pub fn function() {
        println!("called `my_mod::function()`");
    }

    // 在同一模組中,項可以訪問其它項,即使它是私有的。
    pub fn indirect_access() {
        print!("called `my_mod::indirect_access()`, that\n> ");
        private_function();
    }

    // 模組也可以巢狀
    pub mod nested {
        pub fn function() {
            println!("called `my_mod::nested::function()`");
        }

        fn private_function() {
            println!("called `my_mod::nested::private_function()`");
        }

        // 使用 `pub(in path)` 語法定義的函數隻在給定的路徑中可見。
        // `path` 必須是父模組(parent module)或祖先模組(ancestor module)
        pub(in crate::my_mod) fn public_function_in_my_mod() {
            print!("called `my_mod::nested::public_function_in_my_mod()`, that\n > ");
            public_function_in_nested()
        }

        // 使用 `pub(self)` 語法定義的函數則只在當前模組中可見。
        pub(self) fn public_function_in_nested() {
            println!("called `my_mod::nested::public_function_in_nested");
        }

        // 使用 `pub(super)` 語法定義的函數隻在父模組中可見。
        pub(super) fn public_function_in_super_mod() {
            println!("called my_mod::nested::public_function_in_super_mod");
        }
    }

    pub fn call_public_function_in_my_mod() {
        print!("called `my_mod::call_public_funcion_in_my_mod()`, that\n> ");
        nested::public_function_in_my_mod();
        print!("> ");
        nested::public_function_in_super_mod();
    }

    // `pub(crate)` 使得函數隻在當前包中可見
    pub(crate) fn public_function_in_crate() {
        println!("called `my_mod::public_function_in_crate()");
    }

    // 巢狀模組的可見性遵循相同的規則
    mod private_nested {
        pub fn function() {
            println!("called `my_mod::private_nested::function()`");
        }
    }
}

fn function() {
    println!("called `function()`");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn main() {
        // 模組機制消除了相同名字的項之間的歧義。
        function();
        my_mod::function();

        // 公有項,包括巢狀模組內的,都可以在父模組外部訪問。
        my_mod::indirect_access();
        my_mod::nested::function();
        my_mod::call_public_function_in_my_mod();

        // pub(crate) 項可以在同一個 crate 中的任何地方訪問
        my_mod::public_function_in_crate();

        // pub(in path) 項只能在指定的模組中訪問
        // 報錯!函數 `public_function_in_my_mod` 是私有的
        //my_mod::nested::public_function_in_my_mod();

        // 模組的私有項不能直接訪問,即便它是巢狀在公有模組內部的

        // 報錯!`private_function` 是私有的
        //my_mod::private_function();

        // 報錯!`private_function` 是私有的
        //my_mod::nested::private_function();

        // 報錯! `private_nested` 是私有的
        //my_mod::private_nested::function();
    }
}

上面的內容90%以上整理自:

  • https://course.rs/basic/crate-module/intro.html

一本神一樣的 Rust 語言聖經!

多個目錄間模組引用

前面給出的例子大多都是在單個模組中引用;

本小節來看一看在不同目錄之間的引用;

看一下目錄結構:

$ tree .                      
.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── main.rs
    └── user_info
        ├── mod.rs
        └── user.rs

3 directories, 9 files

rust約定在目錄下使用mod.rs將模組匯出;

看一下user.rs的程式碼:

#![allow(unused)]
fn main() {
#[derive(Debug)]
pub struct User {
    name: String,
    age: i32
}

impl User {
    pub fn new_user(name: String, age: i32) -> User {
        User{
            name,
            age
        }
    }
    pub fn name(&self) -> &str {
        &self.name
    }
}

pub fn add(x: i32, y: i32) -> i32 {
    x + y 
}
}

然後在mod.rs裡匯出:

pub mod user;

main.rs呼叫:

mod user_info;
use user_info::user::User;

fn main() {
    let u1 = User::new_user(String::from("tom"), 5);
    println!("user name: {}", u1.name());
    println!("1+2: {}", user_info::user::add(1, 2));
}

多個Cargo之間進行引用

最後,再來看看多個 Cargo 項目之間的引用;

首先分別建立一個可執行項目和一個庫項目:

cargo new multi-crate
cargo new utils --lib

在utils庫中,已經生成了程式碼:

#![allow(unused)]
fn main() {
pub fn add(left: usize, right: usize) -> usize {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}
}

在我們的二進制庫的Cargo.toml引入該庫:

[dependencies]
utils = { path = "../utils", version = "0.1.0" }

path就是庫項目的路徑;

main.rs使用use引入就可以使用了:

use utils::add;

fn main() {
    let x = add(1, 2);
    println!("utils::add(1, 2): {}", x);
}

附錄

原始碼:

  • https://github.com/JasonkayZK/rust-learn/tree/project-structure

Rust 與記憶體

本文譯自 Rust &TheMachine by Prolific K

譯者前言

對於 Rust 初學、尤其是之前慣用記憶體回收器語言(例如 Go、Python、Java)的開發者來說,Rust 提出的各種智慧指標種類繁多,各有其使用情境與限制,光是最單純的借用機制就常在生命週期限制下卡成一團,初學時面臨的各種編譯錯誤不免令人萌生退意。本文作者試圖從底層運作原理上解釋,並提出一些使用指標與架構上的原則;就算偷懶硬背其結論而不去深入理解,仍能維持一定的開發流暢性。

本文不免用到部份 C/C++ 用語,雖非理解本文所必要,仍鼓勵非 C/C++ 背景的開發者們在讀後試著瞭解一下那些概念。許多譯文用語儘量貼近本國用語,然而部份用字可能保留原文,以利讀者自行找尋第一手原文參考資料:

  • 計算機科學中的 stack 和 heap 與其相關操作方法 pop & push
  • Rust 語法定義的關鍵名詞,例如 trait

作者前言

本文主要是寫給正在入門 Rust、已理解基本的語法、卻正與編譯器纏鬥的開發者。我們將從底層的機器運作模型為出發點進行討論,如此更有助於讓人脫離對執行期垃圾回收機制的倚賴。我相信這樣的解釋方式比毫無頭緒地逐條解釋編譯資訊更有效率。

**Rust 的記憶體管理機制相當巧妙,語意表現上很容易讓人忽略自己正在做什麼。這種設計導致編譯器錯誤報告相當不直覺。**如果你對於計算機組織有基本的認識,知道函式的執行與記憶體互動原理,或是樂於學習這方面的知識,這篇文章將有效助你理解並流暢地使用 Rust。

一些心理準備

如果你想縱觀 Rust 全貌,你一定得把以下幾個要點牢記在腦海中:

  • 從組合語言層級理解函式呼叫的運作原理。至少請記得什麼是 call stack指標與 stack 之間的關係、以及 stack 在記憶體中的運作方式。如果你需要復習一下,YouTube 上可以找到一些關於計算機組織的教學影片。
  • Heap 是一堆片段的記憶體區塊,而 stack 是有序、線性、且總是隻從一端變化的記憶體空間。Heap 上可能產生不連續的遺棄資料。
  • 多執行緒的情境,縱使每個執行緒各有各的 stack,但仍共用同一個 heap。
  • 程式指令與靜態資料在程式開始時便一直存在,且不會被移動,因此我們總是能引用這些靜態資料。

妥善管理 Stack 上的資料

「擁有權」這種抽象概念體現於許多行為上,例如記憶體空間管理和指標操作。編譯器檢驗擁有權的同時,也保證我們永遠不會創造出無效的指標、或忘記回收記憶體空間。記憶體空間管理與擁有權之間的關係很直覺,如果我們不使用指標,RAII 資源回收機制會幫我們善後;編譯器鮮少在這種使用情境抱怨什麼。

然而,有時為了實作某些精巧的演算法,或為了避免在呼叫函式時一再複製資料,而希望在程式多處存取同一記憶體位址。我們確實需要指標。每當指標指涉的位址未隨著資料移動或銷毀,無效指標因而誕生。在 Rust 中,除了移動語意以外所有的擁有權合法性則規設計都為防止在 stack 中指涉無效資料。

接下來的內容,首先,我們會先來看看所有權與借用概念如何用在 stack 的運作上。接著,釐清所有在 heap 上放的資料資料所屬的 stack,我們得以將這些資料的生命週期與所有權反映在 stack 上。這些映射關係使我們得以清楚地確定資料銷毀的時間點,同時確保不會持有無效指標。

從正確的 Stack Pop 順序開始

注意:包括 x86 在內的架構設計上,stack 是從高位址往低位址成長,與電腦科學討論用的慣例有微妙的不同;因此在本文中,我們使用「上游(upstream)」與「下游(downstream)」這兩個字眼:呼叫函式進入「下游」,而後向「上游」傳回結果,回到本來發動呼叫的地方。

借用都是往下游遞送,以指向上游的資料。&T 總是指向上游 stack 內的資料 T,並總是比其指涉本體還要早被 pop 出去。

  • 最單純常見的情境,&T 出現於下游,且指向上游的 T
  • 將擁有權整合進資料結構時也適用上述情境。當 &T 指向一個結構內的成員 T,其後又將 &T 塞進其它結構體,則任何擁有 &T 的東西應往下游傳遞,並指回上游的 T
  • 如此這般,T 總是在所有的 &T 都消失之後才被 pop 出 stack。
  • 複雜結構的使用情境常常很難讓編譯器相信上述原則沒有被打破,單純情境則總是易於操作

在 pop 間保證因果關係

編譯器不容許在產生資料之後向其上游傳遞借用。這條規則有很多誤觸方式,包括試圖傳回一個區域資料的借用、在原資料生命週期不夠長的情況下把其借用塞進另一個資料結構、或是在現有出借行為未結束前試圖移動原資料。

以下是參數與傳回值傳遞時對於不同種資料擁有權的規則:

  • &T(借用)可以向下作為函式傳入參數,其內容指涉上層資料。
  • T(擁有)可向下作為函式傳入參數。它可作為傳回值再度移回上層,或是隨著 stack 一同被回收。
  • 在所在函式內產生的資料 T 可以被移往上層(離開當前 stack)或與該層 stack 共存亡。
  • &T 指涉的對象是所在函式內擁有的資料,則此借用不可作為傳回值送進上層。只有資料所有權本身可以往上傳回,因為該資料本身會隨著函式結束而消失,進而使得 &T 指向不合法的位址。
  • 如果該 &T 本已作為參數被傳入,那麼同樣也能傳回上層。

資料間的相對關係會在函式呼叫間保留,這些關係因此可在不同 stack 之間跨層傳遞。一般而言,資料都會往上游(過去)指涉,而向下遊(未來)傳送或夾帶。你也可以把這些規則歸納成兩條:

  • 傳入借用或所有權
  • 傳回所有權,或是指向仍存在的上游資料的借用

好,現在你開始有個底。我們現在知道 stack 上資料的正確清除順序,也為此訂下一些規則以確保不會發生意外。現在讓我們來看看 heap。

利用 Box 把擁有權放在 Heap 上

Box<T> 是一種擁有權指標,其內部有一個指標,指向一塊放在 heap 上的資料。Box<T> 是一個放在 stack 上的 Box 與放在 heap 上的 T 的合成物。它實際上就是在 heap 上的一塊你擁有的資料。你可以傳遞它、移動它、改變它所指涉的目標,也可以將之作為傳回值。當它被銷毀時,它所對應在 heap 上的空間、資料內擁有的其它資料也會同時被回收。Box 的借用(&Box)同前節所述可以被向下傳遞、而只能在不超過 Box 壽命範圍內向上傳遞。

你可以籍由呼叫 Box::new(myT) 以把 T 移上 heap。在呼叫之前,資料都還位於 stack 上,而在呼叫後搬上 heap。Arc::new(myT) 是另一種把資料搬上 heap 的容器,一樣是在產生容器的過程把資料搬上去。Vec::with_capacity(usize) 是一種可變大小的 heap 空間,你隨時可以籍由 push 把資料放上去;Vec 的所有權在你手上,而其內資料被 Vec 擁有。

  • Stack 擁有 heap
  • 當 stack 失去其所擁有的 heap → heap 上的資料隨之銷毀

受到 Box 掌握的 heap 會一直存活到 Box 壽命結束。一般而言,heap 上的空間當視為 stack 的衍伸空間,而 stack 本身的生命週期相當簡單直覺。

即便你弄出像是 Box<…Box<Box<T>>…> 的東西,把一堆指標一起扔上 heap,這些東西依然受到最外層的容器 Box 所掌握,最外層容器仍位於 stack 上,結繫在函式的能見範圍裡,終將隨著函式的結束而一個一個跟著被回收。另一方面,就算籍由 Box::new(&myT) 把借用扔上 heap,你仍無法只留下資料所有權的同時把這個包裝用的 Box 當作傳回值。把借用放上 heap 並不會把讓 &T 憑空變出可傳遞性。

這些抽象概念讓我們幾乎不需要手動處理記憶體區塊的配置與釋放。你仍可以利用 std::mem 來配置記憶體,但這並非常見手法。

引用計數的生命週期

我們必須來特別探討一下 RcArcAtomic reference counter,原子化引用計數)在 heap 與 stack 的使用情境。我們無法確定某些資料需要存在多久,因此將之放上 heap 並以引用計數來管理。Arc 是原子化計數,因此可跨執行緒使用;Rc 則是一般性把資料放上 heap 的版本。這些引用計數容器像是放在 stack 上的遙控器,可以任意傳遞傳回,而且可以多次複製,只有在最後一個備份被銷毀時才會動手清除其內指涉的資料。這種容器本身並不提供其內資料的可變性,但你可以把可變資料包裝後塞在裡面。

借用的生命週期 = 不可移動的區間

當資料已出借的期間內,其本體便不可被移動。當一個借用指向被移動過的資料,實際上就是一個懸置指標。RcArc 便是可行的替代方案,可以任意地被複製(copy / clone)其所指涉內容,或是傳遞引用計數本身。另一個選項則是一開始就把所有權設計在上游,如此一來下游都只是在移動借用。

將多個所有權結合在一起,就會產生生命週期註記

每當你創造東西,它就會有生命週期;但如果它引用指涉了另一個不同生命週期的東西,你就開始需要生命週期註記(lifetime annotation)。這些註記與所有權息息相關。編譯器會檢查這些註記與所有權關係是否與實際所需生命週期一致。

最直接的例子,你試著設計一種資料結構,而這種結構有個成員借用了另一些資料,編譯器會馬上開始問生命週期相關細節。如果你有兩個引用,編譯器會試圖釐清這兩個引用對象的生命週期究竟是否相同。生命週期註記的存在就是為了計算這些資訊。

函式呼叫需要事先確定所需空間

當編譯器發出 unsized types 警告時,是在告訴我們它需要這些容量資訊來規劃 stack 上的記憶體分配方式。編譯器可以處理任意數量的函式呼叫或記憶體對齊等細節,但若無法確定資料型態所佔容量,則編譯器將不知如何將它塞進 stack

  • 需要把未知容量資料作為傳入參數?請用 &Box 或其它已知容量的容器包裝它
  • 需要把未知容量資料作為傳回值?我們無法將區域變數的引用(&)傳出它的存在範圍,請改用 Box 包裝它
  • 借用(&)只是一種指標,而指標的大小是固定的:fn (foo: & dyn Trait) { .. }
  • 陣列大小要在執行期才能決定?或是大小有變動的可能性?你需要 heap,意即需要一個 Box
  • 數值實際型別可能有好幾種?或是需要把不同型別的東西放在同一個容器內?Vec<Box<dyn Trait>> 可以幫你盛裝各種不同但皆實作某一 Trait 的資料,而 foo (t: & dyn Trait) 這樣的函式允許傳入各種已實作特定 Trait 的資料
  • 記住:像是 Vec::push 等等各種容器操作都須先決定傳入值的容量資訊

編譯器可能為了各種不能確定資料大小的狀況而抱怨。Box 本身的大小是確定的,且是一種資料擁有者。每當無法在編譯期確定資料大小時,請善用 Box 把那些資料放上 heap。

Struct 也需要佔用空間

函式的傳入與傳回值都是由你設計的,你可以推算得出容量資訊。對於那些特殊容量的資料型態(譯註:例如 &[T; length]),其容量標註在最後的欄位裡面。

引入物件導向設計

從前在寫有垃圾回收機制的語言,你可以把一堆相關的資料與處理邏輯通通塞進一個 class,隔絕內外部狀態變換邏輯,並享受分割 API 帶來的好處。然而在 Rust 的世界裡,所有權借用的概念迫使我們必須連帶考量資料大小與存在時間相關要素。C 和 C++ 的開發者們過去利用指標來處理這類事務。

我可以定義 struct。這些 struct 各有其對應的成員方法,讓我處理各式 &selfselfSelf,且在此還有些叫作 trait object 的玩意。雖說沒有繼承機制,但我可以用這些語言特性實作物件導向設計嗎?

可以,但也不能完全自由發揮。千萬不要在可解藕生命週期的情況下把不相關的資料包裝在一起。如果一個借用目標的生命週期與你的 struct 不同,你便不得不開始面對那些炫炮難搞的生命週期註記。請儘量從那些較為迷你單純的 struct 開始設計,並把其對應的成員方法介面單純化,以利未來使用在不同的地方。

千萬不要從建構式裡面借用資料

#![allow(unused)]
fn main() {
DontBorrowFromConstructorScopeValues::new()
}

建構式也只是一種函式。在不得不撰寫複雜的建構式時,記住 T&T 的傳遞原則。千萬別從區域變數產生資料之後又將它的借用塞進另一個 struct。你的資料結構必須直接擁有那些資料,或是借用自上游。

一般來說,你可以不要在建構式裡面借用自身成員;千萬別這樣搞自己,你將無法輕易移動這種資料結構的擁有權,除非用上某些奇技淫巧。如果資料成員以這種方式借用,其生命週期會對不起來。你只能把結構性資料向下遊借出,進入結構內部,傳回已借用的部份資料借用,或是再向下游傳遞借用。

我真的需要多態(polymorphism)嗎?

如果你需要在一段程式裡面輪替使用一些不同型別實作的同一種方法,且這種方法屬於特定的 trait(trait 可以說是在 Rust 裡面最接近 interface 語意的介面宣告),則這是使用 trait object 的好機會。當你利用 dyn 關鍵字將變數綁定特定的 trait 而非型別,trait object 隨之產生。

#![allow(unused)]
fn main() {
fn polycaller(thing: Box<dyn Trait>) {
thing.trait_method()
}
}

以上這種函式呼叫便是一種多態的表現,實際上的行為是利用 vtable(virtual table, 虛擬函式表)在執行期決定其行為。舉例來說,Vec<Box<dyn Trait>> 可以用以儲存一連串實作同一 trait 的各種物件。Trait object 幫助我們統一並限制這些物件的行為。

一個常令入門者意外的點是,由於這種執行期的型別不確定性,因此必須籍由借用、或包裝後放上 heap 才能作為參數傳遞;畢竟編譯器在安排函式呼叫行為前必須先確定 stack 容量需求,像這種必須執期行動態查找 vtable 才能決定實際呼叫對象的運作機制是無法直接編譯的。

容器的選用幾乎可以視為語言層面的文法問題

你在使用變數時需要考量的點在於,資料應存在哪種空間裡、如何避免資料競爭、是否需要再次覆寫、需要多長的生命週期。變數借用只是一種文法表現,不產生額外成本,卻保留了執行期顯式或隱式函式呼叫的彈性。C 語言中等同於 Box 的寫法基本上就只是個指標,而 Rust 中 Box::new(myT) 語意上卻也包括了 malloc。這就是為何當初設計語言時僅將這些機制設計成容器型別,而不直接嵌進文法。

TL;DR 以下這幾種常見容器的使用時機:

Single Thread 單一執行緒

  • 可能有多個所有權持有者、且資料放在 heap 上:Rc<T>
  • 不可變引用、但未來可能需要所指涉對象的內容可變性:Cell<T>
  • 需要對不可複製(copy)對象保有內部可變性、或是該對象內部亦有保有內部可變性需求:&mut self RefCell<T>

Multi-Threaded 跨執行緒

  • 可能有多個所有權持有者、且資料放在 heap 上:Arc<T>
  • 不但多持有者、且讀寫行為具有排它性:Arc<Mutex<T>>
  • 不但多持有者、寫入行為具排它性、但允許共時讀取:Arc<RwLock<T>>

筆者認為除了《The Rust Programming Language》以外,這是將容器使用方法解釋得最好的一篇文章

Rust 的世界中,多執行緒環境下的情境更為單純

由於可以移入、甚至在不同執行緒間傳遞的資料種類相當有限,你可以用在跨執行緒的邊界處理手段反而比前述 stack 與 heap 更為單純。此其中唯一的難點在於,如何將本來用在單一執行緒內部的資料組合,運用在多執行緒情境。

執行緒彼此不分享 stack

執行緒彼此無法預測其它執行緒上資料的生命週期,因此把指標指向其它執行緒 stack 內的資料是不安全的。想在這樣的情境下共享資料,勢必得讓資料以某種形式存在於 heap 上。

你用 clone 產生並操作那些放在 stack 裡的 Arc,實際資料與引用計數則理所當然位於 heap 裡。這樣的操作介面使得各執行緒得以各自處理資料生命週期。

而所有需要跨執行緒共用的指標,其所指目標也都必須放在 heap 上,不可在 stack 裡。

遞送並非共享

  • Sync 是可用來跨執行緒分享的 trait
  • Send 則是用來跨執行緒移動的 trait

所謂的 send 行為,大致是先將資料擁有權移進一個緩衝區,而後交給另一個執行緒,從緩衝區再度移進該執行緒的 stack。遞送一個 Box 並非罕見行為。當使用一些第三方 crate 自定義的資料型別時,可用在跨執行緒的型別通常會宣告成 Send + Sync;但當 API 只想將這類特性用於內部實作時,也可以籍由隱藏 SendSync 以避免使用者誤用。

確保只有一個擁有者 + 可移動或可復製 ≈ 可以在執行緒間遞送

關鍵字:send。BoxstructCell 這種單一擁有者、可移動的資料,可以被包裝並透過像是 std::sync::mpsc::channel 之類的介面進行傳遞。

具有 Copy 性質的資料,通常是一些我們不在乎其是否為同一個體的資料(例如 777u32),也總是可以被傳遞。

正在被借用的資料不可被移動,當然也不可被傳遞。某些特定型別不允許移動,也因此無法被傳遞。

Rc 就是一種非固定擁有者的例子。你無法確定現在有多少使用者,無法將其轉換為單一擁有者的資料型別,因此無法將它傳遞到別的執行緒上。其內部的引用計數計算並不保證原子性,以致無法正確地跨執行緒追蹤資料生命週期。

執行緒間只能在資料擁有權與可變性的分享上保證原子性

就算只讀不寫,你仍必須保證資料在需要的時候依舊存在。為了避免造成未定義行為,你必須防止資料競爭;而如前文所述,你不能直接使用那些把生命週期結繫在另一個 stack 上的東西,結論就是你必須用 Arc 這種既可複製又可傳遞的包裝,來跨 stack 存取資料。

那些本身即具原子性的資料型別,籍由正確安全的資料存取設計,讓我們得以在擁有權與生命週期系統的管制下安全地使用它們,並進而預防資料競爭問題。

跨執行緒分享 ≈ 在 heap 上具有原子性的東西

每當你想在執行緒間分享某些資料,因為 Rust 不會放任你做出孕釀資料競爭的溫床,你遲早會發現最直覺的做法即是採用那些已設計用來保證原子性的型別,或是它們的衍伸型別,例如 ArcAtomicBoolMutex 等等。

最容易在 heap 上處理這類事務的就是 Arc。當你需要自動且正確地處理布林值時,Arc<AtomicBool> 是最佳選擇,或是用 Arc 包裝的其它原子性型別。AtomicBool 和那些定義在 std::sync::atomic 裡的型別們,在多執行緒情境下非常好用。

至此,我們只用了相當小的篇幅討論 Rust 底層視角下的跨執行緒資料分享。在這個情境中,所有的可用性與可變性都被限制包裝在原子性容器之中,而資料內容變動與生命週期的維護都發生在 heap 上。以執行緒安全為前題,最易變、最不可測的資料無疑會以最受限的 API 所管制。

進階閱讀

作者後記:為何從底層運作為出發點?

The Rust book》試圖用一種語言設計理論的角度解釋資料生命週期,讓你從抽象後的行為開始學習,而非出於執行機制。該著作有提到一些 heap,但對於已理解底層運作的高階語言使用者而言,這樣的解釋方式確實易使人感到無所適從。在釐清底層機器運作與高階學術性質模型之間的關聯之前,語言學習者很容易受到編譯器的震憾教育。

類似於底層機器運作式的心智模型對於學習有莫大幫助;雖然純抽象理論式學習遲早能讓人上手,但個人認為這種作法的學習效率不佳。機器運作相關知識不難入門,且裨益良多,系統程式開發者遲早需要學習底層運作機制,因此簡化的執行模型對於程式開發者而言有不錯的解釋效果。

部份抱怨提到,這篇文章掩飾了許多編譯器理論上可以達成、在語言理論上合理卻違反直覺或難以簡單解釋的部份。直接解釋運作原理可以讓人快速吸收概念,且不含任何欺瞞成份。另一方面,如果擁有權與生命週期的抽象概念比機器運作更為基礎,我們便難以檢視其概念與抽象機模型之間概念轉換的完整性。直接從完全抽象概念作為出發點也許並不那麼理想。

C 和 C++ 背景的開發者們對於這類艱澀且大量的底層資訊可能習以為常;但對於 Java 和 .Net 業界人士、甚至 Python 或 JavaScript 使用者而言,無論變數允不允借用、哪些東西可否用於參數或回傳值、如何跨執行緒傳遞資料,這些細節在過去常被忽略,因此在討論這類議題時我們不得重新從底層運作從頭闡述。

我必須承認,在這篇文章中仍有許多關於執行緒的細節未討論,且忽略 Rust 中那些標註為 unsafe 的部份功能。如果對於初入此領域者沒有幫助,這部份就不會被納入本文討論範圍,這也是為何我將環狀引用問題與 Weak<T> 相關章節移除的原因。

譯者後記

本文旨在解釋生命週期運作概念、因應這些設計需求而生的資料型別的使用時機與禁忌。文中提到容器種類繁多,或許一時難以牢記,以下這張小抄或許有助快速複習:Rust Memory Container Cheat-sheet

Container cheatsheet

變量聲明和賦值

在Rust中,使用let關鍵字聲明變量,使用等號進行賦值。類型可以自動推斷或顯式指定。

#![allow(unused)]
fn main() {
let x = 10; // 自動推斷類型
let y: f64 = 3.14; // 顯式指定類型
}

在C ++中,使用類型名稱聲明變量,使用等號進行賦值。

int x = 10;
double y = 3.14;

整數運算

在Rust中,整數運算符號使用和C ++相同。但是,除法操作符號使用/而不是C ++中的整數除法符號//。

#![allow(unused)]
fn main() {
let x = 10;
let y = 3;
let z = x + y;
let w = x * y;
let q = x / y; // 此處使用/操作符號
}

在C ++中,整數運算符號使用和Rust相同。

int x = 10;
int y = 3;
int z = x + y;
int w = x * y;
int q = x / y;

浮點數運算

在Rust中,浮點數運算符號使用和C ++相同。

#![allow(unused)]
fn main() {
let x = 3.14;
let y = 2.71;
let z = x + y;
let w = x * y;
let q = x / y;
}

在C ++中,浮點數運算符號使用和Rust相同。

double x = 3.14;
double y = 2.71;
double z = x + y;
double w = x * y;
double q = x / y;

布爾運算

在Rust中,布爾運算符號使用and(&&)和or(||)。

#![allow(unused)]
fn main() {
let x = true;
let y = false;
let z = x && y;
let w = x || y;
}

在C ++中,布爾運算符號使用and(&&)和or(||)。

bool x = true;
bool y = false;
bool z = x && y;
bool w = x || y;

條件語句

在Rust中,if / else語句可以作為表達式使用,並且必須包含在大括號內。

#![allow(unused)]
fn main() {
let x = 10;
if x < 5 {
    println!("x is less than 5");
} else {
    println!("x is greater than or equal to 5");
}
}

在C ++中,if / else語句不能作為表達式使用,並且可以省略大括號。

int x = 10;
if (x < 5) {
    cout << "x is less than 5" << endl;
} else {
    cout << "x is greater than or equal to 5" << endl;
}

迴圈

迴圈 在Rust中,for循環可用於迭代集合,並且可以使用range運算符號創建集合。

#![allow(unused)]
fn main() {
for i in 0..5 {
    println!("{}", i);
}

let arr = [1, 2, 3, 4, 5];
for i in arr.iter() {
    println!("{}", i);
}
}

在C ++中,for循環可用於迭代集合,並且可以使用range運算符號創建集合。

for (int i = 0; i < 5; i++) {
    cout << i << endl;
}

int arr[] = {1, 2, 3, 4, 5};
for (int i : arr) {
    cout << i << endl;
}

函數定義和調用

在Rust中,函數定義使用fn關鍵字,並且可以指定參數和返回類型。

#![allow(unused)]
fn main() {
fn add(x: i32, y: i32) -> i32 {
    x + y
}

let result = add(2, 3);
}

在C ++中,函數定義使用函數名稱,並且可以指定參數和返回類型。

int add(int x, int y) {
    return x + y;
}

int result = add(2, 3);

字符串處理

在Rust中,字符串是utf8編碼的unicode字符集,使用&str類型表示。

#![allow(unused)]
fn main() {
let s1 = "hello";
let s2 = "world";
let s3 = format!("{} {}", s1, s2);
println!("{}", s3);
}

在C ++中,字符串是char類型的數組,使用std :: string類型表示。

#include <string>

std::string s1 = "hello";
std::string s2 = "world";
std::string s3 = s1 + " " + s2;
cout << s3 << endl;

指標

在Rust中,指針是具有所有權語義的智能指針,使用&和*運算符號。

#![allow(unused)]
fn main() {
let x = 10;
let p = &x;
let y = *p;
}

在C ++中,指針是一個可以存儲變量地址的變量,使用*和&運算符號。

int x = 10;
int* p = &x;
int y = *p;

類別

在Rust中,類是結構體struct和實現trait的組合,使用impl關鍵字實現方法。

#![allow(unused)]
fn main() {
struct Person {
    name: String,
    age: i32,
}

impl Person {
    fn new(name: &str, age: i32) -> Self {
        Person { name: name.to_string(), age }
}

fn say_hello(&self) {
    println!("Hello, my name is {} and I am {} years old.", self.name, self.age);
}
    
let person = Person::new("Alice", 30);
person.say_hello();
}

在C ++中,類是具有成員變量和成員函數的結構,使用class關鍵字定義。

#include <iostream>
#include <string>

class Person {
public:
    Person(const std::string& name, int age) : name(name), age(age) {}

    void say_hello() {
        std::cout << "Hello, my name is " << name << " and I am " << age << " years old." << std::endl;
    }

private:
    std::string name;
    int age;
};

Person person("Alice", 30);
person.say_hello();

命名空間

在C ++中,命名空間是一種將名稱分類為一個範圍的方式,可以避免名稱衝突。

namespace math {
    const double PI = 3.14159265358979323846;

    double sin(double x) {
        // ...
    }

    double cos(double x) {
        // ...
    }
}

double x = math::PI;
double y = math::sin(x);

在Rust中,沒有傳統意義上的命名空間,但可以使用模塊來組織代碼,並且可以使用pub關鍵字公開模塊內的項目。

#![allow(unused)]
fn main() {
mod math {
    pub const PI: f64 = 3.14159265358979323846;

    pub fn sin(x: f64) -> f64 {
        // ...
    }

    pub fn cos(x: f64) -> f64 {
        // ...
    }
}

let x = math::PI;
let y = math::sin(x);
}

結構體

在C ++中,結構體是一種自定義的數據類型,可以包含多個成員變量。

struct Point {
    double x;
    double y;
};

Point p = { 1.0, 2.0 };
double x = p.x;
double y = p.y;

在Rust中,結構體struct也是一種自定義的數據類型,可以包含多個字段。

#![allow(unused)]
fn main() {
struct Point {
    x: f64,
    y: f64,
}

let p = Point { x: 1.0, y: 2.0 };
let x = p.x;
let y = p.y;
}

枚舉

在C ++中,枚舉是一種自定義的數據類型,可以包含多個常量值。

enum Color {
    Red,
    Green,
    Blue,
};

Color c = Color::Green;

在Rust中,枚舉enum也是一種自定義的數據類型,可以包含多個變體variant。

#![allow(unused)]
fn main() {
enum Color {
    Red,
    Green,
    Blue,
}

let c = Color::Green;
}

泛型

在C ++中,泛型是一種將代碼寫成可以處理多種數據類型的方式。

template <typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

int x = max(1, 2);
double y = max(1.0, 2.0);

在Rust中,泛型是一種類型參數化的方式。

fn max<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
    if a > b {
        a
    } else {
        b
    }
}

fn main() {
    let x = max(1.0f32, 2.0f32);
    let y = max(1.0f64, 2.0f64);
}

介面

在Rust中,Trait是一種定義方法簽名的接口。

trait Animal {
    fn name(&self) -> &'static str;
    fn make_sound(&self) -> &'static str;
}

struct Dog;
impl Animal for Dog {
    fn name(&self) -> &'static str {
        "Dog"
    }

    fn make_sound(&self) -> &'static str {
        "Bark"
    }
}

struct Cat;
impl Animal for Cat {
    fn name(&self) -> &'static str {
        "Cat"
    }

    fn make_sound(&self) -> &'static str {
        "Meow"
    }
}

fn main() {
    let animals: [&dyn Animal; 2] = [&Dog, &Cat];

    for animal in animals.iter() {
        println!("{} says {}", animal.name(), animal.make_sound());
    }
}

在C ++中,類似的概念是接口interface。

#include <iostream>

class Animal {
public:
    virtual const char* name() = 0;
    virtual const char* make_sound() = 0;
};

class Dog : public Animal {
public:
    const char* name() override { return "Dog"; }
    const char* make_sound() override { return "Bark"; }
};

class Cat : public Animal {
public:
    const char* name() override { return "Cat"; }
    const char* make_sound() override { return "Meow"; }
};

int main() {
    Animal* animals[2] = { new Dog(), new Cat() };

    for (int i = 0; i < 2; i++) {
        std::cout << animals[i]->name() << " says " << animals[i]->make_sound() << std::endl;
    }

    return 0;
}

Ownership and Borrowing

在 Rust 中,每個值都有一個擁有者(Owner),該擁有者負責管理其值的生命週期,並自動在不再需要該值時銷毀它。

fn print_string(s: String) {
    println!("{}", s);
}

fn main() {
    let s = String::from("Hello, Rust!");

    print_string(s);

    // s has been moved and is no longer valid
    // println!("{}", s); // error: use of moved value: `s`
}

在C++中,類似的概念是對象的所有權(Ownership),但是C++沒有自動回收機制,因此需要使用智能指針等手段來管理資源的生命週期。

#include <iostream>
#include <memory>
#include <string>

void print_string(const std::string& s) {
    std::cout << s << std::endl;
}

int main() {
    std::unique_ptr<std::string> s = std::make_unique<std::string>("Hello, C++!");

    print_string(*s);

    // s will be automatically destroyed when it goes out of scope
    return 0;
}

在 Rust 中,為了避免所有權所有權被移動,可以使用引用(Reference)。引用是對某個值的參考,而不擁有該值本身。

fn print_string(s: &String) {
    println!("{}", s);
}

fn main() {
    let s = String::from("Hello, Rust!");

    print_string(&s);

    // s is still valid
    println!("{}", s);
}

在C++中,引用與Rust中的引用類似,但在C++中引用是非空的,而且可以在函數中更改值。

#include <iostream>
#include <string>

void print_string(const std::string& s) {
    std::cout << s << std::endl;
}

int main() {
    std::string s = "Hello, C++!";

    print_string(s);

    // s is still valid
    std::cout << s << std::endl;

    return 0;
}

在Rust中,為了同時允許讀取和寫入某個值,可以使用可變引用(Mutable Reference)。

fn add_one(mut x: &mut i32) {
    *x += 1;
}

fn main() {
    let mut x = 0;

    add_one(&mut x);

    // x is now 1
    println!("{}", x);
}

在C++中,類似的概念是指針(Pointer)。指針是一種特殊的變數,其值是另一個變數的地址。

#include <iostream>

void add_one(int* x) {
    (*x)++;
}

int main() {
    int x = 0;

    add_one(&x);

    // x is now 1
    std::cout << x << std::endl;

    return 0;
}

Pattern Matching

在 Rust 中,可以使用模式匹配(Pattern Matching)來進行分支處理。模式匹配可以用於匹配不同類型的值,比如整數、枚舉、結構體、元組等。

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
}

fn handle_message(msg: Message) {
    match msg {
        Message::Quit => println!("Quit"),
        Message::Move { x, y } => println!("Move to ({}, {})", x, y),
        Message::Write(text) => println!("Write '{}'", text),
    }
}

fn main() {
    let msg1 = Message::Quit;
    let msg2 = Message::Move { x: 10, y: 20 };
    let msg3 = Message::Write(String::from("Hello"));

    handle_message(msg1);
    handle_message(msg2);
    handle_message(msg3);
}

在C++中,也可以使用switch語句來進行分支處理,但是switch語句只能匹配整數值。

#include <iostream>
#include <string>

enum class Message { Quit, Move, Write };

struct MoveMessage {
    int x, y;
};

void handle_message(Message msg) {
    switch (msg) {
        case Message::Quit:
            std::cout << "Quit" << std::endl;
            break;
        case Message::Move:
            {
                MoveMessage move_msg = {10, 20};
                std::cout << "Move to (" << move_msg.x << ", " << move_msg.y << ")" << std::endl;
            }
            break;
        case Message::Write:
            std::cout << "Write 'Hello'" << std::endl;
            break;
    }
}

int main() {
    Message msg1 = Message::Quit;
    Message msg2 = Message::Move;
    Message msg3 = Message::Write;

    handle_message(msg1);
    handle_message(msg2);
    handle_message(msg3);

    return 0;
}

在Rust中,模式匹配也可以用於解構(Deconstruction)元組和結構體。

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let point = Point { x: 10, y: 20 };

    match point {
        Point { x, y } => println!("({}, {})", x, y),
    }

    let tuple = (1, "hello");

    match tuple {
        (i, s) => println!("({}, {})", i, s),
    }
}

在C++中,也可以使用解構來獲取元組和結構體的成員。

#include <iostream>
#include <tuple>
#include <string>

struct Point {
    int x, y;
};

int main() {
    Point point = {10, 20};
    std::tie(std::ignore, std::ignore, point.y) = point;

    std::cout << "(" << point.x << ", " << point.y << ")" << std::endl;

    std::tuple<int, std::string> tuple = std::make_tuple(1, "hello");
    int i;
    std::string s;
    std::tie(i, s) = tuple;

    std::cout << "(" << i << ", " << s << ")" << std::endl;

    return 0;
}

智能指針

Rust 中有三種智能指針:Box、Rc、Arc。

Box 表示一個指向堆上的 T 類型值的指針,它是唯一所有權的。Box 主要用於解決擁有大量資料或者需要在運行時創建出來的資料的所有權問題。Box 的內存佈局和指針類似,只是多了一個 vtable 指向 T 的方法表。

fn main() {
    let x = Box::new(42);

    println!("{}", x);
}

在 C++ 中也有智能指針,主要有 unique_ptr、shared_ptr 和 weak_ptr。

unique_ptr 表示一個唯一所有權的指針,它負責釋放 T 類型對象的內存。unique_ptr 可以通過 move 轉移所有權,也可以使用 std::move 函數轉移所有權。

#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> x(new int(42));

    std::cout << *x << std::endl;

    return 0;
}

shared_ptr 表示一個共享所有權的指針,多個 shared_ptr 可以指向同一個 T 類型對象,它們共享 T 類型對象的所有權。shared_ptr 使用引用計數的方式來實現所有權管理,當引用計數為 0 時,shared_ptr 會自動釋放對象的內存。

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> x(new int(42));

    std::cout << *x << std::endl;

    return 0;
}

weak_ptr 表示一個弱引用指針,它不擁有對 T 類型對象的所有權,可以從一個 shared_ptr 構造出來。

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> x(new int(42));
    std::weak_ptr<int> y(x);

    std::cout << *x << std::endl;
    std::cout << *y.lock() << std::endl;

    return 0;
}

Trait 和 Interface

在 Rust 中,Trait 是一個定義方法的集合,可以實現多態性。一個類型可以實現多個 Trait,實現 Trait 的類型必須實現 Trait 中定義的方法。Trait 可以與泛型一起使用,讓函數和類型更加通用。

trait Printable {
    fn print(&self);
}

struct Point {
    x: i32,
    y: i32,
}

impl Printable for Point {
    fn print(&self) {
        println!("({}, {})", self.x, self.y);
    }
}

fn print_all<T: Printable>(list: Vec<T>) {
    for item in list {
        item.print();
    }
}

fn main() {
    let list = vec![Point { x: 1, y: 2 }, Point { x: 3, y: 4 }];
    print_all(list);
}

在 C++ 中,Interface 是一個包含純虛函數的抽象基類,純虛函數沒有實現,需要子類實現。Interface 可以實現多態性,也可以與泛型一起使用,讓函數和類型更加通用。

#include <iostream>
#include <vector>

class Printable {
public:
    virtual void print() = 0;
};

class Point : public Printable {
public:
    Point(int x, int y) : x(x), y(y) {}

    void print() override {
        std::cout << "(" << x << ", " << y << ")" << std::endl;
    }

private:
    int x, y;
};

template<typename T>
void print_all(std::vector<T>& list) {
    for (auto& item : list) {
        item.print();
    }
}

int main() {
    std::vector<Point> list = { Point(1, 2), Point(3, 4) };
    print_all(list);

    return 0;
}

OO

Rust 支持面向對象編程,但與 C++ 相比,其面向對象特性較為簡化。Rust 的結構體可以包含方法,但是不能繼承和多態,也沒有 C++ 中的訪問控制和友元等機制。

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

fn main() {
    let c = Circle { x: 0.0, y: 0.0, radius: 2.0 };
    println!("The area of the circle is {}", c.area());
}

C++

#include <iostream>
#include <cmath>

class Circle {
private:
    double x;
    double y;
    double radius;

public:
    Circle(double _x, double _y, double _r) : x(_x), y(_y), radius(_r) {}

    double area() {
        return M_PI * (radius * radius);
    }
};

int main() {
    Circle c(0.0, 0.0, 2.0);
    std::cout << "The area of the circle is " << c.area() << std::endl;
    return 0;
}

異常處理

Rust 和 C++ 都支援異常處理,但是 Rust 推崇使用 Result 類型來處理錯誤,而 C++ 則常常使用 try-catch 塊來捕獲和處理異常。

use std::fs::File;
use std::io::prelude::*;
use std::io::Error;

fn read_file(path: &str) -> Result<String, Error> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn main() {
    let path = "test.txt";
    match read_file(path) {
        Ok(contents) => println!("The contents of the file are:\n{}", contents),
        Err(err) => eprintln!("Error reading file: {}", err),
    }
}
#include <iostream>
#include <fstream>

void read_file(const std::string& path) {
    std::ifstream file(path);
    if (!file.is_open()) {
        throw std::runtime_error("Error opening file");
    }
    std::string line;
    while (getline(file, line)) {
        std::cout << line << std::endl;
    }
}

int main() {
    std::string path = "test.txt";
    try {
        read_file(path);
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}

Rust 中的 Trait

在 Rust 中,Trait 是一種定義共享行為的方式,可以看作是 C++ 中的介面(interface)或抽象基類(abstract base class)。它們用於定義一組必須由實現該 trait 的類型提供的方法。以下是 Rust 中 Trait 和 C++ 中類似概念的一些比較:

在 Rust 中,Trait 定義了一組方法,這些方法可以由實現該 trait 的類型提供。以下是一個簡單的例子:

trait Animal {
    fn make_sound(&self);
}

struct Dog;

impl Animal for Dog {
    fn make_sound(&self) {
        println!("Woof!");
    }
}

struct Cat;

impl Animal for Cat {
    fn make_sound(&self) {
        println!("Meow!");
    }
}

fn main() {
    let dog = Dog;
    let cat = Cat;
    
    dog.make_sound();
    cat.make_sound();
}

C++ 中的介面/抽象基類

在 C++ 中,可以使用抽象基類來實現類似的功能。抽象基類是包含純虛擬函式的類,這些函數必須由派生類實現。以下是一個簡單的例子:

#include <iostream>

class Animal {
public:
    virtual void makeSound() const = 0; // 純虛函數
    virtual ~Animal() = default; // 虛析構函數
};

class Dog : public Animal {
public:
    void makeSound() const override {
        std::cout << "Woof!" << std::endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() const override {
        std::cout << "Meow!" << std::endl;
    }
};

int main() {
    Dog dog;
    Cat cat;
    
    dog.makeSound();
    cat.makeSound();
    
    return 0;
}

比較

  1. 定義和實現
    • 在 Rust 中,使用 trait 關鍵字定義 trait,使用 impl 關鍵字為類型實現該 trait。
    • 在 C++ 中,使用抽象基類和純虛擬函式來定義介面,並在派生類中實現這些虛擬函式。
  2. 靜態 vs 動態分發
    • Rust 默認使用靜態分發(即在編譯時決定函數呼叫)。可以使用特徵對象(dyn Trait)來實現動態分發。
    • C++ 使用虛擬函式表(vtable)來實現動態分發。
  3. 所有權和生命週期
    • Rust 中的 trait 和實現需要遵循所有權和生命週期規則,以確保記憶體安全。
    • C++ 中的抽象基類和派生類遵循不同的記憶體管理模型,需要顯式處理記憶體分配和釋放。
  4. 泛型和特化
    • Rust 的 trait 可以與泛型結合使用,提供更多的靈活性和擴展性。
    • C++ 中的範本和特化也提供了類似的功能,但實現方式和語法不同。

通過這些比較,可以看出 Rust 的 trait 和 C++ 的抽象基類在概念上類似,但在具體實現和使用方式上有顯著差異。

Rust 中的 bin, lib, rlib, a, so 概念介紹

出處:https://cloud.tencent.com/developer/article/1583081

寫了這麼久的 Rust 程式碼了,可能很多人還對 Rust 的編譯後的檔案格式不是很清晰。本篇我們就來理一下,Rust 中的 bin, lib, rlib, a, so 是什麼,如何生成,以及其它一些細節。

從 cargo new 說起

我們建立一個新工程,通常從下面兩句入手:

cargo new foobar

複製

cargo new --lib foobar

複製

前者建立一個可執行工程,而後者建立一個庫工程。

實際上,你去探索上述命令列生成的檔案,發現它們的 Cargo.toml 完全一樣,區別僅在於 src 目錄下,可執行工程是一個 main.rs,而庫工程是一個 lib.rs。

這是因為 main.rs 和 lib.rs 對於一個 crate 來講,是兩個特殊的檔案名稱。rustc 內建了對這兩個特殊檔案名稱的處理(當然也可以通過 Cargo.toml 進行組態,不詳談),我們可以認為它們就是一個 crate 的入口。

可執行 crate 和庫 crate 是兩種不同的 crate。下面我們就來一併說一下它們的兄弟姐妹及其之間的異同。

crate type

執行

rustc --help|grep crate-type

複製

可得到如下輸出

       --crate-type [bin|lib|rlib|dylib|cdylib|staticlib|proc-macro]

複製

才發現,原來有這麼多種 crate type。下面挨個看一下。

bin

二進制可執行 crate,編譯出的檔案為二進制可執行檔案。必須要有 main 函數作為入口。這種 crate 不需要在 Cargo.toml 中或 --crate-type 命令列參數中指定,會自動識別。

lib

庫 crate。它其實並不是一種具體的庫,它指代後面各種庫 crate 中的一種,可以認為是一個代理名稱(alias)。

通常來講,如果什麼都不組態,默認指的是 rlib, 會生成 .rlib 的檔案。

rlib

rlib 是 Rust Library 特定靜態中間庫格式。如果只是純 Rust 程式碼項目之間的依賴和呼叫,那麼,用 rlib 就能完全滿足使用需求。

rlib 實現為一個 ar 歸檔檔案。

> file target/debug/libfoobar.rlib
target/debug/libfoobar.rlib: current ar archive

複製

rlib 中包含很多 metadata 資訊(比如可能的上游依賴資訊),用來做後面的 linkage。

在 Cargo.toml 中組態:

[lib]
name = "foobar"
crate-type = ["rlib"]

複製

可以指定生成 rlib,但是一般沒必要設定,因為默認 lib 就是 rlib。

rlib 是平臺(Linux, MacOS, Windows ...)無關的。

dylib

動態庫。

在 Cargo.toml 中組態:

[lib]
name = "foobar"
crate-type = ["dylib"]

複製

會在編譯的時候,生成動態庫(Linux 上為 .so, MacOS 上為 .dylib, Windows 上為 .dll)。

動態庫是平臺相關的庫。動態庫在被依賴並連結時,不會被連結到目標檔案中。這種動態庫只能被 Rust 寫的程序(或遵循 Rust 內部不穩定的規範的程序)呼叫。這個動態庫可能依賴於其它動態庫(比如,Linux 下用 C 語言寫的 PostgreSQL 的 libpq.so,或者另一個編譯成 "dylib" 的 Rust 動態庫)。

cdylib

C規範動態庫。

在 Cargo.toml 中組態:

[lib]
name = "foobar"
crate-type = ["cdylib"]

複製

與 dylib 類似,也會生成 .so, .dylib 或 .dll 檔案。但是這種動態庫可以被其它語言呼叫(因為幾乎所有語言都有遵循 C 規範的 FFI 實現),也就是跨語言 FFI 使用。這個動態庫可能依賴於其它動態庫(比如,Linux 下用 C 語言寫的 PostgreSQL 的 libpq.so)。

staticlib

靜態庫。

在 Cargo.toml 中組態:

[lib]
name = "foobar"
crate-type = ["staticlib"]

複製

編譯會生成 .a 檔案(在 Linux 和 MacOS 上),或 .lib 檔案(在 Windows 上)。

編譯器會把所有實現的 Rust 庫程式碼以及依賴的庫程式碼全部編譯到一個靜態庫檔案中,也就是對外界不產生任何依賴了。這特別適合將 Rust 實現的功能封裝好給第三方應用使用。

proc-macro

過程宏 crate.

在 Cargo.toml 中組態:

[lib]
name = "foobar"
crate-type = ["proc-macro"]

複製

這種 crate 裡面只能匯出過程宏,被匯出的過程宏可以被其它 crate 引用。


Crate type 以及它們之間的區別就介紹到這裡了,有些細節還是需要仔細理解的。本篇意在闡述一些基礎知識,而不打算成為一篇完整的參考檔案,如要查看 Rust Linkage 的詳細內容,直接訪問 Rust Reference。

https://doc.rust-lang.org/reference/linkage.html

這一篇帖子非常有用:

https://users.rust-lang.org/t/what-is-the-difference-between-dylib-and-cdylib/28847

給 C++ 使用者的 Rust 簡介:物件導向篇

物件導向程式設計 (Object-Oriented Programming) 目前仍然是最主流的編程範式 (Programming Paradigm),因此 Rust 也提供了物件導向程式語言最核心的幾項功能:封裝、繼承、多型。然而,在 Rust 中定義成員函式的語法與 C++ 有段差距,它的物件模型與實現繼承的方法也明顯異於 C++ 或大多數主流的物件導向程式語言。在這篇文章中,我將會詳細說明在 Rust 中實現物件導向的方法。

封裝

在之前文章的範例中,我們經常使用 struct,讀者可能會以為 Rust 就如同 C++ 那般,所有 struct 成員都是公開的 (public)。但實際上,Rust 的 struct 成員僅對同一個模組 (module) 內的程式碼公開。而所謂的模組,其實就是 C++ 的命名空間 (namespace):

mod mylib {
    struct Rational { // 有理數
        num: i32, // 分子
        den: i32, // 分母
    }

    fn new_rational() -> Rational {
        return Rational { num: 3, den: 5 }; // OK
    }
}

fn main() {
    let r = mylib::Rational { num: 3, den: 5 }; // error: struct `Rational` is private
}

mod 關鍵字非常接近 C++ 的 namespace,它除了用來區分命名空間以避免撞名,還具備了封裝的功能。在 mylib 中的符號是不對外開放的,只有在同一個模組中的函式才能使用。

我們可以用 pub 關鍵字來公開符號:

mod mylib {
    pub struct Rational {
        num: i32,
        den: i32,
    }

    pub fn new_rational(num: i32, den: i32) -> Rational {
        return Rational { num: num, den: den };
    }
}

fn main() {
    let r = mylib::new_rational(3, 5); // OK
    println!("fraction number is {}/{}", r.num, r.den);  // 錯誤:num 與 den 皆未公開
}

成員函式與建構式

物件導向中的封裝原則告訴我們:使用者不需要也不應該去了解物件內部的實作細節,因此型別的設計者應該禁止使用者直接存取內部成員,而是讓他們透過公開的成員函式進行操作。

我們可以用 impl 關鍵字為某個型別定義成員函式:

mod mylib {
    pub struct Rational {
        num: i32,
        den: i32,
    }

    fn gcd(mut x: i32, mut y: i32) -> i32 {
        // 求取 x 與 y 的最大公因數,這裡省略實作
        // ...
    }

    impl Rational {
        pub fn new(n: i32, d: i32) -> Self {
            Rational { num: n, den: d }
        }

        pub fn is_integer(&self) -> bool {
            self.den == 1
        }

        pub fn reduce(&mut self) {
            let d = gcd(self.num, self.den);

            self.num /= d;
            self.den /= d;
        }
    }
}

fn main() {
    let r = mylib::Rational::new(3, 5);
}

只要對任何型別使用 impl 區塊定義函式,這些函式就會成為該型別的成員函式[1]。以這個例子來說,Rational 型別就有三個成員函式:newis_integerreduce。而成員函式的第一個參數,則代表的這個成員函式如何影響物件:

  • 如果第一個參數是 &self,表示這個成員函式不會改變物件內的狀態,相當於 C++ 中以 const 修飾的成員函式 (const member function)。self 在 Rust 中也是個關鍵字,相當於 C++ 中的 this。需注意的是,在成員函式中也必需透過用 self 才能存取物件的其它成員。
  • 如果第一個參數是 &mut self,表示這是一個會修改物件內部狀態的成員函式,相當於 C++ 中未以 const 修飾的一般成員函式。
  • 如果第一個參數不是 self,表示這是一個靜態成員函式 (static member function)。
  • 除了用 self 代表 this 以外,你還可以用大寫的 Self 來代表這些成員函式所屬的類別,以這個範例來說 Self 就等於 Rational

另外,在 Rust 中物件的建構式[2] (constructor) 其實就是回傳該物件的靜態成員函式,Rust 並未規定建構式要叫什麼名字,不過大部份人會依照慣例,使用 new 這個名字當作建構式。上述的成員函式定義,如果翻譯成 C++ 會長這個樣子:

#![allow(unused)]
fn main() {
namespace mylib {
    class Rational {
    private:
        int32_t num;
        int32_t den;

    public:
        Rational(int32_t n, int32_t d)
            : num(n), den(d)
        { }

        bool is_integer() const {
            return this.den == 1;
        }

        void reduce() {
            auto d = gcd(this.num, this.den);

            this.num /= d;
            this.den /= d;
        }
    }
}
}

繼承與多型

與大多數 OOP 語言不同的是,Rust 並沒有繼承結構成員的功能,你沒辦法如同 C++ 那樣直接讓某個新類別擁有另一個父類別的所有成員[3]。然而 Rust 可以宣告抽象介面,並且指定某個型別必需實作介面中的函式,這點非常接近 Java 或 C# 裡面的 interface

宣告抽象介面的方法,是使用 trait 關鍵字:

#![allow(unused)]
fn main() {
trait JsonObject {
    fn to_json(&self) -> String;
}
}

這相當於用 C++ 宣告一個抽象類別:

#![allow(unused)]
fn main() {
class JsonObject {
public:
    virtual std::string to_json() const = 0;
};
}

你不能在 trait 中宣告成員變數,或是為函式提供實作。Trait 的目的,是為編譯器提供這個型別的介面資訊,不同的 struct 可以提供介面相同,但內容不同的實作,這樣只需要抽換實作,就可以達到程式碼再利用的目的。

你可以用 implfor 關鍵字,讓某個型別實作 trait:

struct Rational {
    num: i32,
    den: i32,
}

impl Rational {
    fn new(n: i32, d: i32) -> Rational {
        Rational { 
            num: n,
            den: d,
        }
    }
}

impl JsonObject for Rational {
    fn to_json(&self) -> String {
        // 因為 JSON 格式中有許多雙引號,我們可以用 r#"..."# 作為格式字串的前後引號
        // r#"abc"def"ghi"# 相當於 "abc\"def\"ghi"
        // 另外大括號在格式字串中有特殊意義,因此需要使用 {{ 或 }} 表達單一的大括號
        format!(r#"{{ "num":{},"den":{} }}"#, self.num, self.den)
    }
}

fn main() {
    let r = Rational::new(3, 5);
    println!("serialized result: {}", r.to_json()); // { "num":3,"den":5 }
}

當然,你可以用其它型別來實作 JsonObject,它代表了任何可以輸出成 JSON 格式的型別。

#![allow(unused)]
fn main() {
// 表示複數型別
struct Complex {
    real: f64,
    imaginary: f64,
}

impl Complex {
    fn new(r: f64, i: f64) -> Complex {
        Complex {
            real: r,
            imaginary: i,
        }
    }
}

impl JsonObject for Complex {
    fn to_json(&self) -> String {
        format!(r#"{{ "real":{},"imaginary":{} }}"#, self.real, self.imaginary)
    }
}
}

你可以使用 &JsonObject 來代表一個實作出 json() 介面的物件,並且以多型的方式呼叫它:

fn dump_json(obj: &JsonObject) {
    println!("{}", obj.to_json());
}

fn main() {
    let r = Rational::new(5, 7);
    let c = Complex::new(3.0, 4.0);
    dump_json(&r); // { "num":5,"den":7 }
    dump_json(&c); // { "real":3,"imaginary":4 }
}

因為我們不知道具體型別是什麼,因此虛擬函式呼叫 (virtual method invocation) 只能在指向某物件的指標上實現,這也是為什麼 dump_json 的兩個參數型別都是 &JsonObject,而且在呼叫時,我們必需用 & 符號取得 rc 的位址。

使用參考就會受到生命週期的限制,而改用智慧指標可以省去許多麻煩。Rust 的智慧指標與多型操作搭配良好,因此你可以用 Box<JsonObject> 來指向任何實作 JsonObject 介面的物件。

fn dump_json_array(array: &[Box<JsonObject>]) {
    print!("[");
    // 為了正確輸出 JSON 陣列中的逗號,我們使用 split_first()
    // 若陣列包含一個以上的元素,會回傳第一個元素及剩下的片段,否則傳回 None
    match array.split_first() {
        Some((first, remain)) => {
            print!("{}", first.to_json());   // 第一個元素前不需加逗號
            for obj in remain.iter() {
                print!(",{}", obj.to_json());// 剩下的元素需要以逗號區隔
            }
        }
        None => (), // array 為空
    }
    println!("]");
}

fn main() {
    let mut v: Vec<Box<JsonObject>> = Vec::new();
    v.push(Box::new(Rational::new(5, 7)));
    v.push(Box::new(Complex::new(3.0, 4.0)));
    dump_json_array(&v);
}

注意在兩個 push 操作中,我們實際上推了兩個不同型別的元素進去,但因為 RationalComplex 都是 JsonObject 的子型別,因此 Box<Rational>Box<Complex> 可以安全地轉型成 Box<JsonObject>,並且放進同一個容器中。當然,因為我們推了兩個不同型別的元素,導致 Rust 無法正確推導出 Vec 容器的型別,因此我們在宣告時必需明確指出 v 的型別是 Vec<Box<JsonObject>>

擴充基本型別

Rust 的基本型別與自訂型別地位相同,你也可以替基本型別定義成員函式,甚至讓他實作某個 trait。比如說,我們可以讓最常見的 i32 實作 JsonObject

#![allow(unused)]
fn main() {
impl JsonObject for i32 {
    fn to_json(&self) -> String {
        self.to_string()
    }
}
}

或是讓內建的 String 型別也實作 JsonObject

#![allow(unused)]
fn main() {
impl JsonObject for String {
    fn to_json(&self) -> String {
        // 為字串前後加上雙引號,並加上跳脫字元
        format!("\"{}\"", self.replace("\\", "\\\\").replace("\"", "\\\""))
    }
}
}

這意味 Box<i32>Box<String> 可以安全地轉型為 Box<JsonObject>

fn main() {
    let mut v: Vec<Box<JsonObject>> = Vec::new();
    v.push(Box::new(1));
    v.push(Box::new(2));
    v.push(Box::new("hello".to_string()));
    v.push(Box::new("world".to_string()));
    dump_json_array(&v); // [1,2,"hello","world"]
}

為某個具體型別實作 trait 有一個限制:為了避免多個實作互相衝突,實作 trait 的 impl 區塊必需與 trait 或是具體型別擺在同一個函式庫 (Rust 稱之為 crate) 當中。簡而言之:

你寫的 trait別人寫的 trait
你寫的型別👌👌
別人寫的型別👌

Trait 的實作

Rust 允許我們擴充所有已存在的型別,讓它們可以實作出新的界面,這點對熟悉 C++ 的讀者來說頗為神祕:為了達成多型,物件中必需有額外欄位指向虛擬函式表 (vtable),才能在執行時期將同名的函式呼叫分派到不同類別的實作當中,如下圖:

C++ 物件有額外欄位指向虛擬函式表

在 C++ 中,由於虛擬函式表的位址固定儲存在物件實體上,因此沒有預留這個空間的內建型別,自然就沒辦法添加任何虛擬成員函式。而其它的類別雖然可以透過多重繼承的方式為其添加界面,但必然要修改既有的程式碼。

Rust 雖然也有虛擬函式表,但並不把它的位址儲存在物件中,而是把它與 trait 參考放在一起,如下圖:

Rust 的 trait 參考結構

從這張圖可以看出,任何指向 trait 的參考,其實會佔用兩個指標,其中一個指向物件實體,另一個指向虛擬函式表。這樣的作法雖然會增加記憶體使用量,但換來了極佳的彈性。即使是其他人製作的型別,你也可以自由擴充它。

Trait 在泛型程式設計 (generic programming) 中也扮演重要角色,我會在後續的文章中詳細介紹。

解構式

你可以實作 Drop trait,這麼一來型別就有了解構式,允許你使用 C++ 中常見的 RAII 手段管理資源。

struct DatabaseSession {
    connection: i32, // connection 代表底層的資源
}

impl DatabaseSession {
    fn new() -> Self {
        DatabaseSession {
            connection: connect(/* ... */), // 連接 server
        }
    }

    fn do_something(&self) {
        // ...
    }
}

impl Drop for DatabaseSession {
    fn drop(&mut self) {
        disconnect(self.connection); // 關閉連線
    }
}

fn main() {
    let session = DatabaseSession::new();
    session.do_something();
    // ...
    // session 離開 scope 時會自動呼叫 drop() 釋放資源
}

所有實作 Drop trait 的物件在生命週期結束時,編譯器會自動為它呼叫 drop() 以釋放資源。當然,即使你沒有實作 Drop,但物件中包含了實作 Drop 的成員,那麼當物件的生命週期結束時,編譯器也會自動地呼叫這些成員的解構式。

在 C++ 中,只要你心臟夠大顆,可以直接呼叫物件的解構式,只是你得自行避免物件在解構後又被拿來用,或是出現重覆解構的情況。在 Rust 中你也可以手動呼叫 drop 來解構物件,但編譯器知道你解構了物件,因此會阻止你做出危險行為。

fn main() {
    let session = DatabaseSession::new();
    // ...
    drop(session);          // 解構 session 物件,釋放其資源

    session.do_something(); // 錯誤:session 已解構
    drop(session);          // 錯誤:session 已解構
                            // 函式結束時不會再呼叫 session 的解構式
}

Move Semantics

在 C++ 中有所謂的「三位一體原則」(rule of three) 或「五位一體原則」(rule of five),意思是如果某個類別定義瞭解構式,那麼一定也要定義出複製建構式 (copy constructor) 並覆載等號賦值 (copy assignment),否則這個物件很容易因為複製出暫時物件,而導致解構式重覆釋放了內部資源。

#![allow(unused)]
fn main() {
class DatabaseSession {
private:
    int connection;

public:
    DatabaseSession()
        : connection( connect(/* ... */) ) // 連接 server
    {}

    ~DatabaseSession() {
        disconnect(connection); // 中斷連線
    }
}

int main() {
    auto session = DatabaseSession();
    auto another = session; // 內部的 connection 被複製了

    return 0;
    // main 結束時,session 與 another 被解構,導致同一個 connection 被重覆關閉
}
}

Rust 沒有這樣的規則。在預設情況下,包括 struct 在內的所有自訂型別都具備 move semantics,因此使用等號賦值,或是用 by-value 方式傳遞到函式內,都會導致所有權轉移。只要變數失去了所有權,在它生命週期結束時就不會呼叫解構式,從而避免重覆釋放資源的問題。

fn main() {
    let s1 = DatabaseSession::new();
    // ...
    let s2 = s1;        // 所有權轉移至 s2 身上
    s1.do_something();  // 錯誤:s1 已失去所有權

    foo(s2);            // s2 所有權轉移到函式中
                        // 函式結束時,s2 所擁有的資源已被釋放

                        // main 結束時,不會呼叫 s1 與 s2 的解構式
}

// 看起來 session 是 call-by-value,但其實應該叫 call-by-move
// 因為呼叫端的所有權傳進了這個函式
fn foo(session: DatabaseSession) {
    session.do_something();
    // 離開函式時會解構 session
}

如果你自己設計了某些方法來複製資源,比如說額外再增加一個連往相同 server 的連線,Rust 的慣例是實作 Clone trait:

impl Clone for DatabaseSession {
    fn clone(&self) -> Self {
        let addr = get_server_info(self.connection);
        return DatabaseSession {
            connection: connect(addr), // 增加一個連往相同目標的連線
        };
    }
}

fn main() {
    let s1 = DatabaseSession::new();
    // ...
    let s2 = s1.clone();    // s2 是新連線
    s1.do_something();      // s1 仍然可用

    // s1 與 s2 是不同連線,都會被解構式釋放
}

有些型別的成員都是單純資料 (POD, plain old data),實作 Clone 時也都只有單純的欄位複製,我們可以用 #[derive(Clone)] 讓編譯器自動幫我們實作出逐欄位複製的 clone()

#[derive(Clone)]
struct Rational {
    num: i32,
    den: i32,
}

fn main()
{
    let r1 = Rational::new(5, 3);
    let r2 = r1.clone();    // r2 直接複製 r1 的所有成員
    let r3 = r1;            // 轉移 r1 所有權到 r3 身上
}

Clone trait 仍然會保留 move semantics,因此使用等號直接賦值時仍然會導致所有權轉移。如果我們想表達型別完全就是 POD,可以直接用等號直接複製其內容,而不需要轉移所有權,只要再加上 Copy trait 即可:

#[derive(Copy,Clone)]
struct Rational {
    num: i32,
    den: i32,
}

fn main() {
    let mut r1 = Rational::new(/* ... */);
    let r2 = r1;    // r2 複製 r1 的內容
    r1.reduce();    // 還是可以繼續使用 r1
    foo(r1);        // 以 call-by-value 的方式呼叫
}

fn foo(r: Rational) {
    // ...
}

Copy 的意思是該型別遇到等號賦值或 call-by-value 的函式傳遞時,會直接呼叫 clone() 創造出複本,因此 CopyClone 的子集合,實作 Copy 的型別一定要實作 Clone

運算子複載 (Operator Overloading)

CopyClone 其實就相當於 C++ 中覆載 (overload) 等號賦值的行為。那麼 Rust 可以覆載其它的運算子嗎?答案是肯定的,而且也是透過 trait。

比如說,實作 Add trait,我們的型別就可以透過加號進行運算:

use std::ops::Add;

impl Add for Rational {
    type Output = Self; // 相當於 typedef Rational Output
    fn add(self, rhs: Self) -> Self {
        Self { // 通分相加
            num: self.num * rhs.den + self.den * rhs.num,
            den: self.den * rhs.den,
        }
    }
}

fn main() {
    let r1 = Rational::new(1, 2);
    let r2 = Rational::new(1, 3);
    let r3 = r1 + r2; // r3 = { num: 5, den: 6 }
}

impl 區塊中,我們除了定義 Rational 的加法外,還定義了 Output 這個型別,代表加法的輸出型別。儘管這個 trait 的成員函式 add() 的回傳型別已經說明瞭 Rational 相加的結果仍然是相同的 Rational,因此這邊定義 Output 似乎有點多此一舉,但我們等一下就會看到它的用處。

當然,我們可以讓 Rational 與其它型別相加:

#![allow(unused)]
fn main() {
impl Add<Complex> for Rational {
    type Output = Complex;
    fn add(self, rhs: Complex) -> Complex {
        // 轉成浮點數後相加
        let f = (self.num as f64) / (self.den as f64);
        return Complex {
            real: f + rhs.real,
            imaginary: rhs.imaginary,
        };
    }
}
}

當不同的型別可以透過運算子覆載進行操作時,往往會讓我們搞不清楚輸出型別,而難以在必要的地方標示型別。比如說我們寫了一個函式把許多 RationalComplex 加起來:

fn sum_all(ra: &[Rational], ca: &[Complex]) -> ? {
    // ...
}

我們知道這兩個型別可以相加,但相加後的型別又是什麼?當然我們可以翻閱文件後填一個正確的型別上去,但如果未來輸出型別有更改,那麼這段函式定義也得跟著改才行。所幸,Add trait 中的 Output 可以幫我們解決這個問題:

#![allow(unused)]
fn main() {
fn sum_all(ra: &[Rational], ca: &[Complex]) -> <Rational as Add<Complex>>::Output {
    // ...
}
}

回傳型別看起來很複雜,它想表達的是「在 RationalAdd<Complex> 的實作中,所定義的 Output 型別」。因此,編譯器會抓出對應的 impl 區塊,找到裡面的 Output 作為這個函式的回傳型別。

儘管運算子覆載是個大家爭論不休的語言功能,但它在泛型程式設計中確實佔了重要地位,有興趣的讀者可以參考 std::ops 的文件,上面列出你可以覆載的運算子。幸運的是,C++ 中邪惡的逗號 (,) 與邏輯運算 (&&||) 並不在其中。[4]

結語

本文介紹了在 Rust 中實現物件導向程式設計的方法。Rust 引入了 trait、禁止類別繼承、又讓自訂型別擁有 move semantics,是與 C++ 相當不同的設計。然而在 RAII 與運算子覆載上,又可以處處看見 C++ 的影子。

在下一篇文章中,我會介紹另一種重要的自訂型別:列舉 (enum),以及搭配它的樣式比對 (pattern matching)。


  1. Rust 使用 method 這個 OOP 中的主流用語來表達成員函式 (member function),不過這篇文章的主要對象是 C++ 使用者,因此我會繼續使用「成員函式」。
  2. Constructor 這個字眼在 functional programming 中具有不同的含義,本篇文章中的 constructor 意指在 OOP 語言中,用來初始化物件的函式。
  3. Rust 鼓勵你用組合 (composition) 代替繼承,但你硬要做的話還是辦得到,方法是直接把一個父類別物件放進成員中,並且用它實作 DerefDerefMut trait。
  4. 覆載這些運算子會改變運算式的求值順序,導致難以名狀的 bug,詳情可參考 C++ 知名教科書 Effective C++

給 C++ 使用者的 Rust 簡介:智慧指標

在絕大多數的程式語言中,在 heap 上配置記憶體都是一項不可或缺的功能。由於存在 stack 上的物件有固定的生命週期,若要建立出像是二元樹 (binary tree) 或連結串列 (linked list) 之類的資料結構,就勢必要把物件配置在 heap 上。

在 Rust 中當然也可以在 heap 上配置物件,但為了正確管理記憶體資源,Rust 不允許你直接取得物件在 heap 上的裸指標 (raw pointer)。相反地,只要直接產生智慧指標 (smart pointer),Rust 就會在 heap 上配置適當的空間、初始化物件、並且把記憶體位址包裝在智慧指標內。

在這篇文章中,我將會介紹在 Rust 中不同的智慧指標,以及它們使用上與 C++ 的相同與相異之處。

最單純的智慧指標 Box

在 Rust 中最簡單的智慧指標是 Box,它相當於 C++ 中的 std::unique_ptr。不同的地方在於你不需要傳遞裸指標來初始化,而是直接傳遞要放在 heap 上的物件內容。

#![allow(unused)]
fn main() {
let p = Box::new(10); // 即 C++14 的 auto p = make_unique<int>(10);
                      // 或 C++11 的 auto p = unique_ptr<int>(new int(10));
}

產生 Box 指標後,你可以用星號來存取這個物件的內容,就像直接操作該物件一樣:

#![allow(unused)]
fn main() {
let mut p = Box::new(10);
println!("{}", *p); // 10
*p += 20;
println!("{}", *p); // 30
}

注意第一行的 mut,若要改變指標指向的物件內容,我們也必需把指標本身宣告為 mut。這個特性稱之為可變繼承 (inherited mutability),在之後介紹 OOP 的文章中,我會解釋為什麼有這樣的規定。

Box 既然是個智慧指標,自然符合 C++ 使用者習慣的 RAII (Resource Acquisition Is Initialization) 模式:在建構式中取得資源,並在解構式中釋放資源。

#![allow(unused)]
fn main() {
fn foo() {
    let p = Box::new(10); // p 配置了一個 i32 的記憶體空間存放 10
    // ...
    // ...
    return; // 離開函式時自動呼叫 p 的解構式,並釋放記憶體
}
}

Box 指標具有和 C++ 的 unique_ptr 相同的特性:對任何配置在 heap 上、使用 Box 管理的物件,只會有一個獨一無二的 Box 指標指向它們。因此 Box 具備了 move semantics,當你使用等號賦值時,該物件的所有權會被轉移到另一個 Box 指標上,這樣才能避免重覆釋放同一個指標。

#![allow(unused)]
fn main() {
{
    let p = Box::new(10);
    let q = p; // p 所控制的物件已轉移到 q 上
    println!("{}", *p); // 錯誤:p 已回到未初始化的狀態
} // 離開區塊,q 被釋放
}

上面的範例中,由於 p 的所有權轉移到 q 身上,因此負責釋放記憶體的責任也轉移到 q 身上。p 不再擁有內容,也不需要在離開區塊時釋放記憶體。

最後,如果 Box 指向某個基本型別,或是任何可以使用 clone() 進行複製的物件,那麼對 Box 指標使用 clone() 將會進行深層複製,新的 Box 指標將會指向原本物件的複本。

#![allow(unused)]
fn main() {
let mut p = Box::new("hello".to_string());
let q = p.clone();
*p += " world";
println!("p = {}", *p); // p = hello world
println!("q = {}", *q); // q = hello
}

空指標與泛型容器 Option

在 Rust 中,不管是參考或智慧指標,都必然指向一個合法存在的物件。那麼我們要用什麼型別來表達一個可能存在、也可能不存在的物件呢?答案是使用 Option 這個泛形類別:

fn check_exist(x: Option<i32>) {
    if x.is_none() {
        println!("x is empty!");
    } else {
        println!("x has value {}", x.unwrap());
    }
}

fn main() {
    check_exist(Some(10)); // x has value 10
    check_exist(None);     // x is empty!
}

Option<T> 是個可以容納零個或剛好一個 T 物件的容器。我們可以用 Some(...)None 來初始化,並使用 is_some()is_none() 來判斷內部是否有值。

unwrap() 可以取出 Option 中的值,然而若在內容為空的時候取值是執行時期錯誤,並導致程式立刻中止。雖然我們可以在呼叫 unwrap() 前使用 is_some() 來確認內部狀態,但 unwrap() 本身仍然會多做一次檢查,因此這樣的寫法並不是 Rust 的慣例 (idiom)。比較好的寫法是使用 pattern matching:

#![allow(unused)]
fn main() {
fn check_exist(x: Option<i32>) {
    match x {
        Some(value) => println!("x has value {}", value),
        None => println!("x is empty!"),
    }
}
}

這邊的 match 區塊很像 C++ 中的 switch case,不同的地方在於 Some(value) 這個寫法,我們可以在 x 內部確實有值的時候,將這個值指定到 value 上。而當 x 內部空空如也時,則執行 None 後面的程式碼。這樣的寫法不但安全,又可以避免在呼叫 unwrap() 時重覆檢查物件內容。

上述的範例中,由於 i32 是可複製的型別,因此 Some(value) 會是 x 的複本。如果我們想要修改 x 的內容,可以用 Some(ref mut value) 取得內容物的參考,表示 value 以 mutable borrow 的方式指向 Option 的內容物。

#![allow(unused)]
fn main() {
let x: Option<i32> = Some(10);
match x {
    Some(ref mut value) => *value += 1, // 當 x 含有某個 i32 時,value 是指向該 i32 的參考
    None => (), // 不做任何事
}
// x 成為 Some(11)
}

Optionmatch 其實就是函數程式語言中的 tagged union 及 pattern matching。這部份我會在後續的文章中介紹,現在我們只需要知道用 Option 來表達「有可能為 null 的型別」即可。

二元樹

有了 BoxOption,我們終於可以做出一個簡單的二元樹 (binary tree):

#![allow(unused)]
fn main() {
struct Node {
    data: i32,
    left: Option<Box<Node>>,
    right: Option<Box<Node>>,
}
}

leftright 的型別看起來有點令人眼花,但這是有原因的:

  1. leftright 可能指向子節點,也可能不指向任何東西。因此我們要使用 Option 來表達這些可能存在也可能為空的欄位。
  2. Option<T> 會直接儲存 T 物件的內容,加上一個額外欄位來區分 Some(T) 亦或是 None,因此 Option<T> 這個物件的大小會略大於 T 的大小。如果我們直接用 Option<Node> 當作 leftright 的欄位形別,就會出現「自己包含自己」的矛盾情況。因此,我們要使用 Box 來表達 Option 的內容型別其實是指向下一層節點的指標,而不是節點本身。

有了定義後,我們可以試著進行操作:

#![allow(unused)]
fn main() {
fn traverse(root: &Node) {
    print!("({} ", root.data);
    match root.left {
        Some(ref child) => traverse(child),
        None => (),
    }
    match root.right {
        Some(ref child) => traverse(child),
        None => (),
    }
    print!(")");
}
}

traverse 示範了使用前序 (prefix) 印出所有元素的方法:首先我們把自己的資料印出來,接著遞迴尋訪左子樹及右子樹。因為尋訪子樹時不需要修改節點的內容,但仍需要指向節點的參考,因此我們用 Some(ref child) 表達這個參考是個 immutable borrow。

接著我們簡單實作加入元素的操作:

#![allow(unused)]
fn main() {
// 產生一個 leaf node
fn new_leaf(data: i32) -> Option<Box<Node>> {
    Some( Box::new(
        Node {
            data: data,
            left: None,
            right: None,
        }
    ))
}

fn insert(root: &mut Node, data: i32) {
    if data > root.data {
        match root.right {
            Some(ref mut child) => insert(child, data),
            None => root.right = new_leaf(data),
        }
    } else {
        match root.left {
            Some(ref mut child) => insert(child, data),
            None => root.left = new_leaf(data),
        }
    }
}
}

這是不考慮二元樹平衡、用最單純的方式實作元素插入。

fn main() {
    let mut root = Node { data: 10, left: None, right: None };
    insert(&mut root, 1);
    insert(&mut root, 20);
    insert(&mut root, 5);
    insert(&mut root, 15);
    traverse(&root); // 印出 (10 (1 (5 ))(20 (15 )))
}

在後續文章中,我會介紹 Rust 的 OOP 功能,讓這個二元樹的界面更接近 C++ 使用者所習慣的物件操作方式。

參考計數指標 Rc

Box 限制了只有一個指標能指向 heap 上的物件。如果你希望能在多個不同地方分享同一個物件,就得改用參考計數指標 Rc,它相當於 C++ 的 shared_ptr

use std::rc::Rc; // Rc 不在預設的命名空間中,因此需要用 use 引入符號
                 // 相當於 C++ 的 using std::shared_ptr;
fn main() {
    let x = Rc::new(10);
    let y = x.clone();
    let z = y.clone();

    println!("{}", *y); // 10
    println!("{}", *z); // 10
}

建立 Rc 指標的方法和 Box 非常接近,然而當你呼叫 clone() 時,Rc 會增加參考計數並產生另一個指向相同物件的指標。注意對 C++ 的 shared_ptr 來說,只要用等號賦值就會增加參考計數,但等號在 Rust 中是轉移所有權的意思,你要明確呼叫 clone() 才會增加參考計數。

use std::rc::Rc;
fn main() {              // // C++ 對照
    let x = Rc::new(10); // auto x = make_shared<int>(10);
    let y = x;           // auto y = move(x);
    println!("{}", *x);  // cout << *x << endl; // 錯誤:x 所有權已轉移
    let z = y.clone();   // auto z = y;
    println!("{}", *y);  // cout << *y << endl; // OK:z 和 y 指向同一物件
}

相較於垃圾回收 (garbage collection),使用參考計數指標管理記憶體的優點在於回收記憶體時不會暫停整支程式,同時也容易控制物件解構的時間點。然而若遇到循環參考 (circular reference),因為每個參考計數都無法歸零,就無法正確釋放所有物件。因此參考計數指標通常還需要搭配弱指標 (weak pointer) 一起使用。

Rust 也提供了弱指標,但因為篇幅有限,我們先跳過這部份,把焦點放在 Rust 遇到的另一個問題上:要如何修改 Rc 指向的物件內容呢?

引入修改能力的 CellRefCell

Rc 不提供任何修改內容的操作界面,即使你宣告為 mut 也無法寫入新值:

#![allow(unused)]
fn main() {
let mut p = Rc::new(10);
*p = 20; // 錯誤
}

為什麼不能改變 Rc 指向的內容呢?我們回想一下 Rust 對參考設下的限制,可以瞭解它背後的設計思維:

Rust 在編譯時會保證,任何變數經過取址後,要嘛同時有許多個 immutable borrow,或是隻存在唯一一個 mutable borrow,不允許兩種取址方法同時存在,也不允許有多個 mutable borrow。

Rc 是指向某物件的智慧指標,因此廣義上也屬於參考型別。同時 Rc 允許多個指標指向同一個物件,因此它必需是 immutable borrow,否則就違反了 Rust 對參考型別所設下的原則。如果你對這個概念還有點模糊,不妨考慮以下的例子:

use std::rc::Rc;
fn foo(v1: Rc<Vec<i32>>, mut v2: Rc<Vec<i32>>) {
    let it = v1.iter(); // 取得 v1 的 iterator
    v2.clear();         // 修改 v2 的內容
}

fn main() {
    let v = Rc::new(vec![1,2,3]);
    foo(v.clone(), v.clone()); // 故意讓 foo() 中的 v1 與 v2 指向同一物件
}

foo() 函式中,我們對 v2 的內容做了更動,會導致所有指向 v2 的迭代器失效。然而 v1v2 在執行時可能會指向同一個物件,因此繼續使用指向 v1 的迭代器可能會造成記憶體存取錯誤。不幸的是,在編譯這段程式碼時,Rust 無從判斷 v1v2 是否指向同一個 Vec 物件,因此 Rust 採用最保守的做法:禁止第四行中對 Rc 內容物的修改行為,來避免可能的記憶體錯誤。

當然,如果共享某塊資料就無法修改其內容,這樣的智慧指標並不是很實用。因此 Rust 提供兩個中介容器,讓你可以透過它們來修改 Rc 指向的物件內容,同時又能維持記憶體安全。我們先來看看適合用在內建型別的 Cell

use std::rc::Rc;
use std::cell::Cell;
fn main(){
    let p = Rc::new(Cell::new(10));
    println!("{}", p.get()); // 10
    p.set(20);
    println!("{}", p.get()); // 20
}

Cell 是一個只容納單一元素的簡單容器,你可以透過 get()set() 來存取,同時具備內部可變 (interior mutability) 的特性,因此就算包在 Rc 內部,仍然可以修改它的內容。Cell 內部只能容納可複製的類別,比如說 i32f32 之類的基本型別[1],而且你只能透過 set() 來修改 Cell 的內容,而無法取得內容物的參考,因此在同一個執行緒內,Cell 可以保證在 set() 修改內容時,不會破壞任何指向內容物的參考。

許多型別具有 move semantics,像是 VecString。這類型別不能放在 Cell 當中,因此 Rust 提供了 RefCell,它提供了 borrow()borrow_mut() 讓你取得內容物的參考,並且在執行時期動態檢查這些參考是否符合 borrow checker 的規則。馬上來看一個範例:

#![allow(unused)]
fn main() {
use std::rc::Rc;
use std::cell::RefCell;

fn foo(c1: Rc<RefCell<Vec<i32>>>, c2: Rc<RefCell<Vec<i32>>>) {
    let v1 = c1.borrow();   // 取得 c1 內部的 Vec 參考
    let it = v1.iter();

    let mut v2 = c2.borrow_mut(); // 取得 c2 內部的 Vec 參考,並標明我們要修改內容
    v2.clear();
}
}

這段程式碼可以通過編譯,然而如果我們傳入指向同一個 RefCell 的指標,將會造成執行時期錯誤:

fn main() {
    let c = Rc::new(RefCell::new(vec![1,2,3]));
    foo(c.clone(), c.clone()); // thread 'main' panicked at 'already borrowed: BorrowMutError'
}

執行時的錯誤訊息會清楚告訴你:在呼叫 c2.borrow_mut() 時,還存在另一個未消滅的 borrow,因此違反了 borrow checker 的規則。

常見問題

RefCell 的使用對 Rust 初學者而言可說是一大挑戰。它使得型別變得很複雜,同時又需要額外操作才能接觸到真正的內容物件。另一個初學者常見的問題是這樣:

#![allow(unused)]
fn main() {
fn foo(c: Rc<RefCell<Vec<i32>>>) {
    let it = c.borrow().iter(); // 取得內容物的 iterator
    // ...
}
}

這段程式碼無法通過編譯。如果你使用 1.16 或更新版本的編譯器,可以看到 Rust 很努力地畫圖解釋:

error: borrowed value does not live long enough
 --> <anon>:5:31
  |
5 |     let it = c.borrow().iter(); // 取得內容物的 iterator
  |              ----------       ^ temporary value dropped here while still borrowed
  |              |
  |              temporary value created here
...
9 | }
  | - temporary value needs to live until here
  |
  = note: consider using a `let` binding to increase its lifetime

錯誤的原因在於 RefCell 使用 RAII 的方式來管理 borrow 狀態。當你呼叫 borrow()borrow_mut() 時,RefCell 會創造出一個代理物件 (proxy object),讓你用這個代理物件存取其內容物。而當這個代理物件的生命週期結束時,其解構式會恢復 RefCell 內的 borrow 狀態,讓你下次可以繼續對 RefCell 呼叫 borrow()borrow_mut()

然而,在上述的範例中,我們直接對 borrow() 的回傳值呼叫 iter(),而沒有把代理物件存在某個變數中。因此代理物件會被放在暫時變數中,並在這個運算式結束時解構,恢復 RefCell 的 borrow 狀態。然而,iter() 的結果卻還存在 it 這個變數內,並且指向內容物,這超出了 RefCell 所設下的保護傘。

正確的作法是把 borrow() 的結果存在變數中:

#![allow(unused)]
fn main() {
fn foo(c: Rc<RefCell<Vec<i32>>>) {
    let r = c.borrow(); // r 的生命週期比 it 長,確保 borrow 存在
    let it = r.iter();
    // ...
}
}

適合多執行緒的 Arc

使用智慧指標會遇到的另一個問題是執行緒安全 (thread safety)。在多執行緒的環境下,同時操作同一個智慧指標的參考計數可能會導致 race condition。因此,Rust 提供了另一個適用於多緒處理的智彗指標 Arc。它的使用方法大致上與 Rc 相同,但在操作參考計數時會使用原子操作 (atomic operation),確保在多執行緒的環境中可以正確管理資源。

ArcRc 同樣限制了修改內容物的能力。然而 CellRefCell 也不適合在多緒環境中使用,因此若你要在多緒環境中共享一塊可修改內容的資料,必需搭配 MutexRwLock 這兩個中介容器。

多緒安全已經超出了本文的討論範圍,有興趣的讀者可以自行參考 Rust 官方文件。幸運的是,borrow checker 的規則有助於你避免許多在多緒環境中的常見錯誤,若你已經習慣了 RefCell 的操作方式,對於 MutexRwLock 也能駕輕就熟。

智慧指標的成本

Box 限制只有一個指標能操作 heap 上的物件,而這項限制在編譯時期就能完成檢查,因此 Box 的大小和裸指標大小相同,操作時的效能也完全相同。

Box 與裸指標的成本完全相同

Rc 除了配置物件外,還會額外配置空間給來儲存兩個數字:強參考計數及弱參考計數。這兩個數字與物件指標形成一塊結構一起儲存在 heap 上,而 Rc 內部只儲存這個結構的指標。因此 Rc 的大小與裸指標相同,但是在複製及消滅時需要額外增減參考計數。

Rc 額外配置了兩個數字的空間

Arc 的結構與 Rc 相同,但在增減參考計數時使用原子操作,因此複製或消減時的時間成本會比 Rc 更高。

另外,Cell<T> 佔用的空間與 T 完全相同,而 RefCell<T> 則需要額外一個空間記錄 borrow 狀態。

Cell 不會佔用額外空間,RefCell 額外佔用一個數字的空間

結語

這篇文章介紹瞭如何在 Rust 中配置 heap 上的記憶體,並且使用智慧指標以確保程式正確回收資源。相較於其它語言,Rust 在這部份的學習曲線很陡峭:只是要分享資料而已,為什麼搞得這麼複雜呢?

其原因在於 Rust 儘可能提供你選擇。C++ 發明人 Bjarne Stroustrup 曾說明過 C++ 的零成本抽象化原則 (zero-overhead rule):若你沒用到某個功能,就不需要為它付出時間或空間上的成本;若你確實用了某個抽象化的功能,那麼編譯器幫你產生的程式碼至少要和你手寫的最佳化程式碼表現得一樣好。在這個設計原則之下,追求執行效率的程式設計師才能安心使用語言提供的高階抽象功能,讓程式容易維護,又能保有良好的效能。

Rust 同樣遵循零成本抽象化原則,這也是它提供各種智慧指標的原因:如果物件不需要共享,就不需要付出參考計數的成本;如果只有單執行緒,就不需要付出原子操作的額外成本;如果不修改分享的資料,就不需要負擔 RefCellMutex 的額外成本。

C++ vs Rust

#include <iostream>
using namespace std;

int main() {
    int a = 10;
    int *ptr1 = &a;
    int **ptr2 = &ptr1;

    const int size = 2;
    int arr[size];
    int *ptr;
    ptr = arr; // 將 ptr 指向陣列

    // 1. 記憶體位址
    for (int i = 0; i < size; i++) {
        std::cout << "&arr[" << i << "]: " << &arr[i] << ", ptr+" << i << ": " << (ptr + i) << std::endl;
    }

    // 2. 值的存取
    arr[0] = 0;
    *(ptr + 1) = 1;
    for (int i = 0; i < size; i++) {
        std::cout << "arr[" << i << "]: " << arr[i] << ", *(ptr+" << i << "): " << *(ptr + i) << std::endl;
    }

    return 0;
}
fn main() {
    let a = 10;
    let ptr1 = &a;
    let _ptr2 = &ptr1;

    const SIZE: usize = 2;
    let mut arr = [0; SIZE];
    let ptr: *mut i32; // 將 ptr 聲明為可變指標
    ptr = arr.as_mut_ptr(); // 將 ptr 指向陣列

    // 1. 記憶體位址
    for i in 0..SIZE {
        println!("&arr[{}]: {:p}, ptr+{}: {:p}", i, &arr[i], i, unsafe { ptr.add(i) });
    }

    // 2. 值的存取
    arr[0] = 0;
    unsafe {
        *(ptr.add(1)) = 1; // 使用 add() 方法計算指標的偏移量並修改值
    }
    for i in 0..SIZE {
        println!("arr[{}]: {}, *(ptr+{}): {}", i, arr[i], i, unsafe { *ptr.add(i) });
    }
}

Copy trait的std文件

Copy trait的std文件

https://doc.rust-lang.org/std/marker/trait.Copy.html

#![allow(unused)]
fn main() {
pub trait Copy: Clone { }
}

只需複製位即可複製其值的類型。

默認情況下,變數繫結具有 move語義。換句話說:

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Foo;

let x = Foo;

let y = x;

// `x` 已移至 `y`,因此無法使用

// println!("{:?}", x); // error: use of moved value
}

但是,如果類型實現 Copy,則它具有複製語義:

#![allow(unused)]
fn main() {
// 我們可以派生一個 `Copy` 實現。
// `Clone` 也是必需的,因為它是 `Copy` 的父特徵。
#[derive(Debug, Copy, Clone)]
struct Foo;

let x = Foo;

let y = x;

// `y` 是 `x` 的副本

println!("{:?}", x); // A-OK!
}

重要的是要注意,在這兩個示例中,唯一的區別是分配後是否允許您訪問 x。 在後臺,複製(copy)和移動(move)都可能導致將位複製到記憶體中,儘管有時會對其進行最佳化。

如何實現 Copy?

有兩種方法可以在您的類型上實現 Copy。最簡單的是使用 derive:

#![allow(unused)]
fn main() {
#[derive(Copy, Clone)]
struct MyStruct;
}

您還可以手動實現 CopyClone:

#![allow(unused)]
fn main() {
struct MyStruct;

impl Copy for MyStruct { }

impl Clone for MyStruct {
    fn clone(&self) -> MyStruct {
        *self
    }
}
}

兩者之間的區別很小: derive 策略還將 Copy 繫結在類型參數上,這並不總是需要的。

CopyClone 有什麼區別?

複製是隱式發生的,例如作為分配 y = x 的一部分。Copy 的行為不可多載; 它始終是簡單的按位複製。

克隆是一個明確的動作 x.clone()Clone 的實現可以提供安全複製值所需的任何特定於類型的行為。 例如,用於 StringClone 的實現需要在堆中複製指向字串的緩衝區。 String 值的簡單按位副本將僅複製指針,從而導致該行向下雙重釋放。 因此,StringClone,但不是 Copy

CloneCopy 的父特徵,因此 Copy 的所有類型也必須實現 Clone。 如果類型為 Copy,則其 Clone 實現僅需要返回 *self (請參見上面的示例)。

類型何時可以是 Copy?

如果類型的所有元件都實現 Copy,則它可以實現 Copy。例如,此結構體可以是 Copy:

#![allow(unused)]
fn main() {
#[derive(Copy, Clone)]
struct Point {
   x: i32,
   y: i32,
}
}

一個結構體可以是 Copy,而 i32Copy,因此 Point 有資格成為 Copy。 相比之下,考慮

#![allow(unused)]
fn main() {
struct PointList {
    points: Vec<Point>,
}
}

結構體 PointList 無法實現 Copy,因為 Vec 不是 Copy。如果嘗試派生 Copy 實現,則會收到錯誤消息:

the trait `Copy` may not be implemented for this type; field `points` does not implement `Copy`

共享引用 (&T) 也是 Copy,因此,即使類型中包含不是 *Copy 類型的共享引用 T,也可以是 Copy。 考慮下面的結構體,它可以實現 Copy,因為它從上方僅對我們的非 Copy 類型 PointList 持有一個 shared 引用:

#![allow(unused)]
fn main() {
#[derive(Copy, Clone)]
struct PointListWrapper<'a> {
    point_list_ref: &'a PointList,
}
}

什麼時候類型不能為 Copy?

某些類型無法安全複製。例如,複製 &mut T 將建立一個別名可變引用。 複製 String 將重複管理 String 緩衝區,從而導致雙重釋放。

概括後一種情況,任何實現 Drop 的類型都不能是 Copy,因為它除了管理自己的 size_of:: 位元組外還管理一些資源。

果您嘗試在包含非 Copy 資料的結構或列舉上實現 Copy,則會收到 E0204 錯誤。

什麼時候類型應該是 Copy?

一般來說,如果您的類型可以實現 Copy,則應該這樣做。 但是請記住,實現 Copy 是您類型的公共 API 的一部分。 如果該類型將來可能變為非 Copy,則最好現在省略 Copy 實現,以避免 API 發生重大更改。

其他實現者

除下面列出的實現者外,以下類型還實現 Copy:

  • 函數項類型 (即,為每個函數定義的不同類型)
  • 函數指針類型 (例如 fn() -> i32)
  • 如果項類型也實現 Copy (例如 [i32; 123456]),則所有大小的陣列類型
  • 如果每個元件還實現 Copy (例如 ()(i32, bool)),則為元組類型
  • 閉包類型,如果它們沒有從環境中捕獲任何值,或者所有此類捕獲的值本身都實現了 Copy。 請注意,由共享引用捕獲的變數始終實現 Copy (即使引用對象沒有實現),而由變數引用捕獲的變數從不實現 Copy

#[derive(Debug)]
struct MyStruct;

impl Copy for MyStruct {
}

impl Clone for MyStruct {
    fn clone(&self) -> MyStruct {
        println!("Cloning MyStruct");
        *self
    }
}

fn main() {
    let original = MyStruct;
    let copied = original;
    let cloned = original.clone();

    println!("Original: {:?}", original);
    println!("Copied: {:?}", copied);
    println!("Cloned: {:?}", cloned);
}

Rust 中的 bin, lib, rlib, a, so 概念介紹

出處: https://blog.51cto.com/u_15683898/5426916

在 Rust 中,常見的可執行檔和庫文件格式包括:

  • 可執行檔(bin):在 Rust 中,可執行檔指的是一個由 Rust 編譯器直接編譯出的二進制 ELF 或 Mach-O 檔案格式文件。可執行檔不能被其他程式直接使用,而只能由終端用戶執行。在 Rust 的項目中,所有的可執行檔都位於 src/bin 目錄中,每個可執行檔都是一個獨立的 Rust 程式。將其命名為 foo.rs 將會產生一個可執行檔 foo
  • 函式庫(lib):在 Rust 中,函式庫指的是一個二進制或共享庫,它提供了一組用於更大程式中使用的函數。函式庫可以在 Rust 程式中被引用,以避免代碼重複。在 Rust 的項目中,所有的函式庫都位於 src/lib 目錄中。根據文件生成不同的庫類型,可以分為靜態庫(.a)和動態庫(.so)。靜態庫被編譯進可執行檔,而動態庫則僅在運行時被加載,並在多個程序之間共享。
  • 靜態庫(rlib):在 Rust 中,靜態庫需要通過 rustc 生成,其文件格式為 RLIB。靜態庫(.rlib)是一個經過 Rust 編譯器優化並靜態鏈接的二進制文件。靜態庫可以直接與 Rust 可執行文件一起複製和發布。與函式庫(.so)相比,靜態庫具有更快的加載時間和更好的可移植性。在 Rust 中,編譯靜態庫需要執行以下命令: rustc --crate-type=staticlib path/to/library.rs
  • 動態庫(so):在 Rust 中,動態庫使用共享庫(.so)文件作為文件格式。動態庫被編譯成一個獨立的文件並可以在多個執行檔之間共享。在 Rust 項目中編譯動態庫需要使用 cargo 的 build 命令,並將 crate-type 設置為 cdylib

在 Rust 中,可執行檔和函式庫都遵循 ELF(可執行和可鏈接格式)標準,該標準應用於 Linux 和其他類Unix系統。因此,這些檔案可以透明地在 Linux 系統上被解析和執行。

在 Linux 系統上,要執行 Rust 可執行檔或函式庫,必須首先確保文件已被正確編譯。對於 Rust 的可執行檔案或函式庫,您可以使用以下命令進行編譯:

  • 可執行檔:將 Cargo.toml 中的 bin 選項設置為正在編譯的可執行檔,然後使用以下命令編譯:

    $ cargo build --bin your_bin_name
    

    或者,使用以下命令在優化模式下編譯:

    $ cargo build --release --bin your_bin_name `
    
  • 函式庫(靜態庫或動態庫):將 Cargo.toml 中的 lib 選項設置為正在編譯的函式庫,然後使用以下命令編譯:

    $ cargo build --lib
    

    或者,使用以下命令編譯動態庫(請注意,crate-type 選項需要設置為 cdylib):

    $ cargo build --release --lib --features cdylib `
    

完成編譯後,您可以使用命令行運行 Rust 的可執行檔,並將函式庫鏈接到其他程式中。在 Linux 上執行 Rust 程式和函式庫與運行任何其他 ELF 文件一樣,只需確保它們是正確生成並遵循了 ELF 規範即可。

在編譯可執行檔或庫文件時,應將文件命名為該文件的用途並使用相應的擴展名,例如:將可執行檔命名為 foo.rs 並編譯生成可執行檔 foo;將靜態庫命名為 libfoo.rs 並執行命令 rustc --crate-type=staticlib libfoo.rs;將動態庫命名為 libfoo.rs 並執行 cargo build --release 命令來編譯動態庫文件 libfoo.so

寫了這麼久的 Rust 程式碼了,可能很多人還對 Rust 的編譯後的檔案格式不是很清晰。本篇我們就來理一下,Rust 中的 bin, lib, rlib, a, so 是什麼,如何生成,以及其它一些細節。

從 cargo new 說起 我們建立一個新工程,通常從下面兩句入手:

#![allow(unused)]
fn main() {
cargo new foobar
}

#![allow(unused)]
fn main() {
cargo new --lib foobar
}

前者建立一個可執行工程,而後者建立一個庫工程。

實際上,你去探索上述命令列生成的檔案,發現它們的 Cargo.toml 完全一樣,區別僅在於 src 目錄下,可執行工程是一個 main.rs,而庫工程是一個 lib.rs。

這是因為 main.rs 和 lib.rs 對於一個 crate 來講,是兩個特殊的檔案名稱。rustc 內建了對這兩個特殊檔案名稱的處理(當然也可以通過 Cargo.toml 進行組態,不詳談),我們可以認為它們就是一個 crate 的入口。

可執行 crate 和庫 crate 是兩種不同的 crate。下面我們就來一併說一下它們的兄弟姐妹及其之間的異同。

crate type

執行

rustc --help|grep crate-type

可得到如下輸出

--crate-type [bin|lib|rlib|dylib|cdylib|staticlib|proc-macro]

才發現,原來有這麼多種 crate type。下面挨個看一下。

bin

二進制可執行 crate,編譯出的檔案為二進制可執行檔案。必須要有 main 函數作為入口。這種 crate 不需要在 Cargo.toml 中或 --crate-type 命令列參數中指定,會自動識別。

lib

庫 crate。它其實並不是一種具體的庫,它指代後面各種庫 crate 中的一種,可以認為是一個代理名稱(alias)。

通常來講,如果什麼都不組態,默認指的是 rlib, 會生成 .rlib 的檔案。

rlib

rlib 是 Rust Library 特定靜態中間庫格式。如果只是純 Rust 程式碼項目之間的依賴和呼叫,那麼,用 rlib 就能完全滿足使用需求。

rlib 實現為一個 ar 歸檔檔案。

file target/debug/libfoobar.rlib target/debug/libfoobar.rlib: current ar archive 1. 2. rlib 中包含很多 metadata 資訊(比如可能的上游依賴資訊),用來做後面的 linkage。

在 Cargo.toml 中組態:

[lib]
name = "foobar"
crate-type = ["rlib"]

可以指定生成 rlib,但是一般沒必要設定,因為默認 lib 就是 rlib。

rlib 是平臺(Linux, MacOS, Windows ...)無關的。

dylib 動態庫。

在 Cargo.toml 中組態:

[lib]
name = "foobar"
crate-type = ["dylib"]

會在編譯的時候,生成動態庫(Linux 上為 .so, MacOS 上為 .dylib, Windows 上為 .dll)。

動態庫是平臺相關的庫。動態庫在被依賴並連結時,不會被連結到目標檔案中。這種動態庫只能被 Rust 寫的程序(或遵循 Rust 內部不穩定的規範的程序)呼叫。這個動態庫可能依賴於其它動態庫(比如,Linux 下用 C 語言寫的 PostgreSQL 的 libpq.so,或者另一個編譯成 "dylib" 的 Rust 動態庫)。

cdylib

C規範動態庫。

在 Cargo.toml 中組態:

[lib]
name = "foobar"
crate-type = ["cdylib"]

與 dylib 類似,也會生成 .so, .dylib 或 .dll 檔案。但是這種動態庫可以被其它語言呼叫(因為幾乎所有語言都有遵循 C 規範的 FFI 實現),也就是跨語言 FFI 使用。這個動態庫可能依賴於其它動態庫(比如,Linux 下用 C 語言寫的 PostgreSQL 的 libpq.so)。

staticlib

靜態庫。

在 Cargo.toml 中組態:

[lib]
name = "foobar"
crate-type = ["staticlib"]

編譯會生成.a 檔案(在 Linux 和 MacOS 上),或 .lib 檔案(在 Windows 上)。

編譯器會把所有實現的 Rust 庫程式碼以及依賴的庫程式碼全部編譯到一個靜態庫檔案中,也就是對外界不產生任何依賴了。這特別適合將 Rust 實現的功能封裝好給第三方應用使用。

proc-macro

過程宏 crate.

在 Cargo.toml 中組態:

[lib]
name = "foobar"
crate-type = ["proc-macro"]

這種 crate 裡面只能匯出過程宏,被匯出的過程宏可以被其它 crate 引用。

Crate type 以及它們之間的區別就介紹到這裡了,有些細節還是需要仔細理解的。本篇意在闡述一些基礎知識,而不打算成為一篇完整的參考檔案,如要查看 Rust Linkage 的詳細內容,直接訪問 Rust Reference。

https://doc.rust-lang.org/reference/linkage.html

這一篇帖子非常有用:

https://users.rust-lang.org/t/what-is-the-difference-between-dylib-and-cdylib/28847

Rust 使用 reqwest 發送 HTTP 請求

use reqwest;
use std::error::Error;
use std::{fs::File, io::copy};
use tokio;

async fn async_call() -> Result<(), Box<dyn Error>> {
    let response = reqwest::get("https://upload.wikimedia.org/wikipedia/zh/3/34/Lenna.jpg").await?;

    if response.status().is_success() {
        let bytes = response.bytes().await?;

        std::fs::write("image_async.jpg", bytes)?;

        println!("async download Lenna.jpg");
    } else {
        println!("Error: {}", response.status());
    }

    Ok(())
}

fn sync_call() -> Result<(), Box<dyn std::error::Error>> {
    let response =
        reqwest::blocking::get("https://upload.wikimedia.org/wikipedia/zh/3/34/Lenna.jpg")?;

    if !response.status().is_success() {
        panic!("response status: {}", response.status());
    }

    let mut file = File::create("Lenna.jpg")?;
    copy(&mut response.bytes().unwrap().as_ref(), &mut file)?;

    println!("sync download Lenna.jpg");

    Ok(())
}


fn main() {
    if let Err(err) = sync_call() {
        eprintln!("sync_call error: {:?}", err);
    }

    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        if let Err(err) = async_call().await {
            eprintln!("async_call error: {:?}", err);
        }
    });
}
[package]
name = "test_reqwest"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
reqwest = { version = "0.12.3", features = ["blocking"] }
tokio = { version = "1.37.0", features = ["full"] }

Blocking implementation

First let’s add the following dependencies using cargo add:

# Add anyhow as a dependency
cargo add anyhow
# Add reqwest with blocking feature
cargo add reqwest -F blocking
use std::{fs::File, io::copy};
use anyhow::Result;

fn download_image_to(url: &str, file_name: &str) -> Result<()> {
    // Send an HTTP GET request to the URL
    let mut response = reqwest::blocking::get(url)?;

    // Create a new file to write the downloaded image to
    let mut file = File::create(file_name)?;

    // Copy the contents of the response to the file
    copy(&mut response, &mut file)?;

    Ok(())
}

fn main() {
    let image_url = "https://www.rust-lang.org/static/images/rust-logo-blk.svg";
    let file_name = "rust-logo-blk.svg";
    match download_image_to(image_url, file_name) {
        Ok(_) => println!("image saved successfully"),
        Err(e) => println!("error while downloading image: {}", e),
    }
}

Non-blocking (async) implementation

In contrast to the first iteration, we can use tokio and leverage non-blocking APIs provided by reqwest to solve the trivia by applying asynchronous programming techniques. First, let’s add the necessary dependencies:

# Add anyhow as a dependency
cargo add anyhow
# Add reqwest with blocking feature
cargo add reqwest -F blocking
# Add tokio with full featureset
cargo add tokio -F full

Having the crates added to our project, we can move on and implement downloading and storing the image from an URL:

use std::{fs::File, io::{copy, Cursor}};
use anyhow::Result;

async fn download_image_to(url: &str, file_name: &str) -> Result<()> {
    // Send an HTTP GET request to the URL
    let response = reqwest::get(url).await?;
    // Create a new file to write the downloaded image to
    let mut file = File::create(file_name)?;
    
    // Create a cursor that wraps the response body
    let mut content =  Cursor::new(response.bytes().await?);
    // Copy the content from the cursor to the file
    copy(&mut content, &mut file)?;

    Ok(())
}

#[tokio::main]
async fn main() -> Result<()> {
    let image_url = "https://www.rust-lang.org/static/images/rust-logo-blk.svg";
    let file_name = "rust-logo-blk.svg";
    match download_image_to(image_url, file_name).await {
        Ok(_) => println!("image saved successfully"),
        Err(e) => println!("error while downloading image: {}", e),
    }
    Ok(())
}
cargo new print-function-name-dump-stack
  • Cargo.toml
[package]
name = "print-function-name-dump-stack"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
stdext = "0.3.1"
backtrace = "0.3.65"
  • main.rs
use backtrace::Backtrace;
use std::process;
use std::thread;
use stdext::function_name;

macro_rules! print_current_info {
    () => {{
        println!(
            "pid={},tid={:?} {}:{} {}",
            process::id(),
            thread::current().id(),
            file!(),
            line!(),
            function_name!()
        );
    }};
}

fn dump_stack_test() {
    let bt = Backtrace::new();
    print_current_info!();
    println!("backtrace dump start ===============");
    println!("{:?}", bt);
}

fn test_func() {
    print_current_info!();
    dump_stack_test();
}

fn main() {
    print_current_info!();
    test_func();
}

如何標準Rust程式碼與Criterion

什麼是基準測試?

基準測試是測試您的程式碼性能的做法,以查看它能做多快(延遲)或多少(吞吐量)工作。 這在軟體開發中常被忽視的步驟是建立和維護快速和高性能程式碼的關鍵。 基準測試為開發人員提供了必要的指標,用於理解他們的程式碼在各種工作負載和條件下的表現如何。 出於防止功能回歸的同樣原因,你應該編寫測試以防止性能回歸。 性能問題也是問題!

用 Rust 編寫 FizzBuzz

為了編寫基準測試,我們需要一些原始碼來進行基準測試。 首先,我們將編寫一個非常簡單的程序, FizzBuzz

FizzBuzz的規則如下:

寫一個程序,列印從 1100 的整數(包含):

  • 對於三的倍數,列印 Fizz
  • 對於五的倍數,列印 Buzz
  • 對於既是三的倍數又是五的倍數的數,列印 FizzBuzz
  • 對於所有其他數,列印這個數字

許多種編寫FizzBuzz的方法。 所以我們會選擇我最喜歡的一種:

fn main() {
    for i in 1..=100 {
        match (i % 3, i % 5) {
            (0, 0) => println!("FizzBuzz"),
            (0, _) => println!("Fizz"),
            (_, 0) => println!("Buzz"),
            (_, _) => println!("{i}"),
        }
    }
}
  • 建立一個 main 函數
  • 1 迭代到 100(含)。
  • 對於每個數字,分別計算 35 的取餘(除後餘數)。
  • 對兩個餘數進行模式匹配。 如果餘數為 0,那麼這個數就是給定因素的倍數。
  • 如果 35 的餘數都為 0,則列印 FizzBuzz
  • 如果只有 3 的餘數為 0,則列印 Fizz
  • 如果只有 5 的餘數為 0,則列印 Buzz
  • 否則,就列印這個數字。

按步驟操作

為了與本教學進行同步學習,您需要 安裝 Rust

🐰 這篇文章的原始碼在 GitHub 上可以找到

安裝好 Rust 後,您可以打開一個終端窗口,然後輸入:cargo init game

然後導航至新建立的 game 目錄。

game
├── Cargo.toml
└── src
    └── main.rs

你應該能看到一個名為 src 的目錄,其中有一個名為 main.rs 的檔案:

fn main() {
    println!("Hello, world!");
}

將其內容取代為上述的 FizzBuzz 實現。然後運行 cargo run。 輸出結果應該像這樣:

$ cargo run   Compiling playground v0.0.1 (/home/bencher)    Finished dev [unoptimized + debuginfo] target(s) in 0.44s     Running `target/debug/game`
12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzz...9798FizzBuzz

🐰 砰!你正在破解程式設計面試!

應該生成了一個新的 Cargo.lock 檔案:

game
├── Cargo.lock
├── Cargo.toml
└── src
    └── main.rs

在進一步探討之前,有必要討論微基準測試和宏碁準測試之間的區別。

微基準測試 vs 宏碁準測試

有兩大類軟體基準測試:微基準測試和宏碁準測試。 微基準測試的操作層次和單元測試類似。 例如,為一個確定單個數字是 FizzBuzz,還是 FizzBuzz 的函數設立的基準測試,就是一個微基準測試。 宏碁準測試的操作層次和整合測試類似。 例如,為一函數設立的基準測試,該函數可以玩完整個 FizzBuzz 遊戲,從 1100,這就是一個宏碁準測試。

通常,儘可能在最低的抽象等級進行測試是最好的。 在基準測試的情況下,這使得它們更易於維護, 並有助於減少測量中的噪聲。 然而,就像有一些端到端測試對於健全性檢查整個系統根據預期組合在一起非常有用一樣, 擁有宏碁準測試對於確保您的軟體中的關鍵路徑保持高性能也非常有用。

在 Rust 中進行基準測試

在 Rust 中常用的基準測試工具有三種: libtest benchCriterion, 以及 Iai

libtest 是 Rust 的內建單元測試和基準測試框架。 儘管 libtest bench 是 Rust 標準庫的一部分,但它仍被認為是不穩定的, 所以它只在 nightly 編譯器版本中可用。 要在穩定的 Rust 編譯器上工作, 需要使用 單獨的基準測試框架。 然而,這兩者都不在積極開發中。

在 Rust 生態系統中,維護最積極的基準測試框架是 Criterion。 它既可以在穩定的 Rust 編譯器版本上運行,也可以在 nightly版本上運行, 它已經成為了 Rust 社區的事實標準。 與 libtest bench 相比,Criterion 還提供了更多的功能。

Criterion 的實驗性替代品是 Iai,它和 Criterion 的創作者是同一個人。 然而,它使用指令數量而不是牆鐘時間: CPU 指令,L1 訪問,L2 訪問以及 RAM 訪問。 這使得它可以進行單次基準測試,因為這些指標在運行間應該保持幾乎一致。

三者都是由Bencher支援的。那麼為什麼要選擇Criterion呢? Criterion是Rust社區的事實標準基準測試工具。 我推薦使用Criterion來測試你的程式碼的延遲。 也就是說,Criterion非常適合測量時鐘時間。

重構 FizzBuzz

為了測試我們的 FizzBuzz 應用,我們需要將邏輯從程序的 main 函數中解耦出來。 基準測試工具無法對 main 函數進行基準測試。為了做到這一點,我們需要做一些改動。

src 下,建立一個新的名為 lib.rs 的檔案:

game
├── Cargo.lock
├── Cargo.toml
└── src
    └── lib.rs
    └── main.rs

將以下程式碼新增到 lib.rs

#![allow(unused)]
fn main() {
pub fn play_game(n: u32, print: bool) {
    let result = fizz_buzz(n);
    if print {
        println!("{result}");
    }
}
pub fn fizz_buzz(n: u32) -> String {
    match (n % 3, n % 5) {
        (0, 0) => "FizzBuzz".to_string(),
        (0, _) => "Fizz".to_string(),
        (_, 0) => "Buzz".to_string(),
        (_, _) => n.to_string(),
    }
}

}
  • play_game:接受一個無符號整數 n,用該數字呼叫 fizz_buzz,如果 printtrue,則列印結果。
  • fizz_buzz:接受一個無符號整數 n,執行實際的 FizzBuzzFizzBuzz 或數字邏輯,然後將結果作為字串返回。

然後更新 main.rs,使其看起來像這樣:

use game::play_game;
fn main() {
    for i in 1..=100 {
        play_game(i, true);
    }
}
  • game::play_game:從我們剛剛用 lib.rs 建立的 game 包中匯入 play_game
  • main:我們程序的主入口點,遍歷從 1100 的數字,對每個數字呼叫 play_game,並將 print 設為 true

對FizzBuzz的基準測試

為了對我們的程式碼進行基準測試,我們需要建立一個benches目錄,並新增一個檔案來包含我們的基準測試,play_game.rs

game
├── Cargo.lock
├── Cargo.toml
└── benches
    └── play_game.rs
└── src
    └── lib.rs
    └── main.rs

play_game.rs中增加下列程式碼:

#![allow(unused)]
fn main() {
use criterion::{criterion_group, criterion_main, Criterion};
use game::play_game;
fn bench_play_game(c: &mut Criterion) {
    c.bench_function("bench_play_game", |b| {
        b.iter(|| {
            std::hint::black_box(for i in 1..=100 {
                play_game(i, false)
            });
        });
    });
}
criterion_group!(benches, bench_play_game,);
criterion_main!(benches);
}
  • 匯入Criterion基準測試運行器。
  • 從我們的game包中匯入play_game函數。
  • 建立一個名為bench_play_game的函數,它接受一個對Criterion的可變引用。
  • 使用Criterion實例(c)來建立一個名為bench_play_game的基準測試。
  • 然後使用基準測試運行器(b)來多次運行我們的宏碁準測試。
  • 在一個”黑箱”中運行我們的宏碁準測試,這樣編譯器就不會最佳化我們的程式碼。
  • 1100包括,進行迭代。
  • 對於每一個數字,呼叫play_game,設定printfalse

現在我們需要組態game包來運行我們的基準測試。

在你的Cargo.toml檔案的底部新增以下內容:

[dev-dependencies]
criterion = "0.5"

[[bench]]
name = "play_game"
harness = false
  • criterion:將criterion新增為開發依賴,因為我們只在性能測試中使用它。
  • bench:註冊play_game作為一個基準測試,並設定harnessfalse,因為我們將使用Criterion作為我們的基準測試工具。

現在我們已經準備好進行基準測試了,運行cargo bench

$ cargo bench
   Compiling playground v0.0.1 (/home/bencher)
    Finished bench [optimized] target(s) in 4.79s
     Running unittests src/main.rs (target/release/deps/game-68f58c96f4025bd4)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/release/deps/game-043972c4132076a9)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running benches/play_game.rs (target/release/deps/play_game-e0857103eb02eb56)
bench_play_game         time:   [3.0020 µs 3.0781 µs 3.1730 µs]
Found 12 outliers among 100 measurements (12.00%)
  2 (2.00%) high mild
  10 (10.00%) high severe

🐰 讓我們調高節拍!我們已經得到了第一個基準測試指標!

最後,我們可以讓我們疲倦的開發者頭腦得到休息… 開玩笑,我們的使用者想要一個新功能!

用 Rust 編寫 FizzBuzzFibonacci

我們的關鍵績效指標(KPI)下降了,所以我們的產品經理(PM)希望我們新增新功能。 經過多次頭腦風暴和許多使用者採訪後,我們決定光有 FizzBuzz 已經不夠了。 現在的孩子們希望有一個新的遊戲,FizzBuzzFibonacci。

FizzBuzzFibonacci的規則如下:

編寫一個程序,列印從 1100 的整數(包括):

  • 對於三的倍數,列印 Fizz
  • 對於五的倍數,列印 Buzz
  • 對於既是三的倍數又是五的倍數的,列印 FizzBuzz
  • 對於是斐波那契數列的數字,只列印 Fibonacci
  • 對於所有其他的,列印該數

斐波那契數列是一個每個數字是前兩個數字之和的序列。 例如,從 01開始,斐波那契數列的下一個數字將是 1。 後面是:2, 3, 5, 8 等等。 斐波那契數列的數字被稱為斐波那契數。所以我們將不得不編寫一個檢測斐波那契數的函數。

許多方法可以編寫斐波那契數列,同樣也有許多方法可以檢測一個斐波那契數。 所以我們將採用我的最愛:

#![allow(unused)]
fn main() {
fn is_fibonacci_number(n: u32) -> bool {
    for i in 0..=n {
        let (mut previous, mut current) = (0, 1);
        while current < i {
            let next = previous + current;
            previous = current;
            current = next;
        }
        if current == n {
            return true;
        }
    }
    false
}
}
  • 建立一個名為 is_fibonacci_number 的函數,該函數接收一個無符號整數,並返回一個布林值。
  • 遍歷從 0 到我們給定的數 n(包含 n)的所有數字。
  • 01 分別作為前一個當前 數字來初始化我們的斐波那契序列。
  • 當前數字小於當前迭代 i 時持續迭代。
  • 新增前一個當前 數字來獲得 下一個 數字。
  • 前一個 數字更新為 當前 數字。
  • 當前 數字更新為 下一個 數字。
  • 一旦 當前 大於或等於給定數字 n,我們將退出循環。
  • 檢查 當前 數字是否等於給定數字 n,如果是,則返回 true
  • 否則,返回 false

現在我們需要更新我們的 fizz_buzz 功能:

#![allow(unused)]
fn main() {
pub fn fizz_buzz_fibonacci(n: u32) -> String {
    if is_fibonacci_number(n) {
        "Fibonacci".to_string()
    } else {
        match (n % 3, n % 5) {
            (0, 0) => "FizzBuzz".to_string(),
            (0, _) => "Fizz".to_string(),
            (_, 0) => "Buzz".to_string(),
            (_, _) => n.to_string(),
        }
    }
}
}
  • fizz_buzz 功能重新命名為 fizz_buzz_fibonacci 以使其更具描述性。
  • 呼叫我們的 is_fibonacci_number 輔助函數。
  • 如果 is_fibonacci_number 的結果為 true,則返回 Fibonacci
  • 如果 is_fibonacci_number 的結果為 false,則執行相同的 FizzBuzzFizzBuzz 或數字邏輯,並返回結果。

因為我們將 fizz_buzz 重新命名為 fizz_buzz_fibonacci,我們也需要更新我們的 play_game 功能:

#![allow(unused)]
fn main() {
pub fn play_game(n: u32, print: bool) {
    let result = fizz_buzz_fibonacci(n);
    if print {
        println!("{result}");
    }
}
}

我們的 mainbench_play_game 功能可以保持完全相同。

對FizzBuzzFibonacci的基準測試

現在我們可以重新運行我們的基準測試了:

$ cargo bench
   Compiling playground v0.0.1 (/home/bencher)
    Finished bench [optimized] target(s) in 4.79s
     Running unittests src/main.rs (target/release/deps/game-68f58c96f4025bd4)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/release/deps/game-043972c4132076a9)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running benches/play_game.rs (target/release/deps/play_game-e0857103eb02eb56)
bench_play_game         time:   [20.067 µs 20.107 µs 20.149 µs]
                        change: [+557.22% +568.69% +577.93%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 6 outliers among 100 measurements (6.00%)
  4 (4.00%) high mild
  2 (2.00%) high severe

哦哦!Criterion向我們顯示了FizzBuzz和FizzBuzzFibonacci遊戲之間性能差距為+568.69%。 你的數字會比我的稍微有些不同。 然而,兩者之間的差距可能在5x的範圍內。 這對我來說看起來是比較好的結果!特別是考慮到我們將像_Fibonacci_這樣的花哨功能新增到我們的遊戲中。 孩子們會喜歡的!

在 Rust 中展開 FizzBuzzFibonacci

我們的遊戲很受歡迎!孩子們確實喜歡玩 FizzBuzzFibonacci。 為此,高層下達了他們想要續集的消息。 但這是現代世界,我們需要的是年度循環收入(ARR),而不是一次性購買! 我們遊戲的新願景是開放性的,不再固定在 1100 之間(即使是包含在內的)。 不,我們正在開拓新的疆域!

Open World FizzBuzzFibonacci的規則如下:

編寫一個程序,它接受_任何_正整數並列印:

  • 對於三的倍數,列印 Fizz
  • 對於五的倍數,列印 Buzz
  • 對於同時是三和五的倍數的,則列印 FizzBuzz
  • 對於是斐波那契數列的數字,只列印 Fibonacci
  • 對於其他所有數字,列印該數字

為了讓我們的遊戲適應任何數字,我們需要接受一個命令列參數。 將 main 函數更新為如下形式:

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let i = args
        .get(1)
        .map(|s| s.parse::<u32>())
        .unwrap_or(Ok(15))
        .unwrap_or(15);
    play_game(i, true);
}
  • 收集所有從命令列傳遞給我們遊戲的參數(args)。
  • 獲取傳遞給我們遊戲的第一個參數,並將其解析為無符號整數 i
  • 如果解析失敗或沒有傳入參數,就默認以 15 作為輸入運行我們的遊戲。
  • 最後,用新解析的無符號整數 i 來玩我們的遊戲。

現在我們可以用任何數字來玩我們的遊戲了! 使用 cargo run 後跟 -- 將參數傳遞給我們的遊戲:

$ cargo run -- 9
   Compiling playground v0.0.1 (/home/bencher)
    Finished dev [unoptimized + debuginfo] target(s) in 0.44s
     Running `target/debug/game 9`
Fizz
$ cargo run -- 10
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/game 10`
Buzz
$ cargo run -- 13
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/game 13`
Fibonacci

如果我們省略或提供了無效的數字:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/game`
FizzBuzz
$ cargo run -- bad
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/game bad`
FizzBuzz

哇,這是一個仔細的測試過程!CI 通過了。我們的上司非常高興。 讓我們發佈吧!🚀

結束

海綿寶寶三週後

一切正常的模因

🐰 … 也許這是你的職業生涯的結束?

開玩笑的!其實一切都在燃燒!🔥

起初,一切看似進行得順利。 但在週六早上02:07,我的尋呼機響了起來:

📟 你的遊戲起火了!🔥

從床上匆忙爬起來後,我試圖弄清楚發生了什麼。 我試圖搜尋日誌,但這非常困難,因為一切都在不停地崩潰。 最後,我發現了問題。孩子們!他們非常喜歡我們的遊戲,以至於玩了高達一百萬次! 在一股靈感的閃現中,我新增了兩個新的基準測試:

#![allow(unused)]
fn main() {
fn bench_play_game_100(c: &mut Criterion) {
    c.bench_function("bench_play_game_100", |b| {
        b.iter(|| std::hint::black_box(play_game(100, false)));
    });
}
fn bench_play_game_1_000_000(c: &mut Criterion) {
    c.bench_function("bench_play_game_1_000_000", |b| {
        b.iter(|| std::hint::black_box(play_game(1_000_000, false)));
    });
}
}
  • 一個用於玩遊戲並輸入數字一百(100)的微基準測試bench_play_game_100
  • 一個用於玩遊戲並輸入數字一百萬(1_000_000)的微基準測試bench_play_game_1_000_000

當我運行它時,我得到了這個:

$ cargo bench
   Compiling playground v0.0.1 (/home/bencher)
    Finished bench [optimized] target(s) in 4.79s
     Running unittests src/main.rs (target/release/deps/game-68f58c96f4025bd4)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/release/deps/game-043972c4132076a9)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running benches/play_game.rs (target/release/deps/play_game-e0857103eb02eb56)
bench_play_game         time:   [20.024 µs 20.058 µs 20.096 µs]
                        change: [-0.0801% +0.1431% +0.3734%] (p = 0.21 > 0.05)
                        No change in performance detected.
Found 17 outliers among 100 measurements (17.00%)
  9 (9.00%) high mild
  8 (8.00%) high severe

bench_play_game_100     time:   [403.00 ns 403.57 ns 404.27 ns]
Found 13 outliers among 100 measurements (13.00%)
  6 (6.00%) high mild
  7 (7.00%) high severe

等待一下… 等待一下…

bench_play_game_1_000_000
                        time:   [9.5865 ms 9.5968 ms 9.6087 ms]
Found 16 outliers among 100 measurements (16.00%)
  8 (8.00%) high mild
  8 (8.00%) high severe

什麼!403.57 ns x 1,000 應該是 403,570 ns 而不是 9,596,800 ns (9.5968 ms x 1_000_000 ns/1 ms) 🤯 儘管我的斐波那契數列程式碼功能上是正確的,我必須在某個地方有一個性能bug。

修復 Rust 中的 FizzBuzzFibonacci

讓我們再次看一下 is_fibonacci_number 函數:

#![allow(unused)]
fn main() {
fn is_fibonacci_number(n: u32) -> bool {
    for i in 0..=n {
        let (mut previous, mut current) = (0, 1);
        while current < i {
            let next = previous + current;
            previous = current;
            current = next;
        }
        if current == n {
            return true;
        }
    }
    false
}
}

現在我在考慮性能,我意識到我有一個不必要的,額外的循環。 我們可以完全擺脫 for i in 0..=n {} 循環, 只需直接比較 current 值和給定的數字 (n) 🤦

#![allow(unused)]
fn main() {
fn is_fibonacci_number(n: u32) -> bool {
    let (mut previous, mut current) = (0, 1);
    while current < n {
        let next = previous + current;
        previous = current;
        current = next;
    }
    current == n
}
}
  • 更新您的 is_fibonacci_number 函數。
  • 01 初始化我們的斐波那契序列作為 previouscurrent 數字。
  • current 數字小於 給定數字 n 時迭代。
  • previouscurrent 數字相加以獲得 next 數字。
  • previous 數字更新為 current 數字。
  • current 數字更新為 next 數字。
  • 一旦 current 大於或等於給定的數字 n,我們將退出循環。
  • 檢查 current 數字是否等於給定的數字 n 並返回該結果。

現在,讓我們重新運行這些基準測試,看看我們做得如何:

$ cargo bench
   Compiling playground v0.0.1 (/home/bencher)
    Finished bench [optimized] target(s) in 4.79s
     Running unittests src/main.rs (target/release/deps/game-68f58c96f4025bd4)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/release/deps/game-043972c4132076a9)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running benches/play_game.rs (target/release/deps/play_game-e0857103eb02eb56)
bench_play_game         time:   [3.1201 µs 3.1772 µs 3.2536 µs]
                        change: [-84.469% -84.286% -84.016%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 5 outliers among 100 measurements (5.00%)
  1 (1.00%) high mild
  4 (4.00%) high severe

bench_play_game_100     time:   [24.460 ns 24.555 ns 24.650 ns]
                        change: [-93.976% -93.950% -93.927%] (p = 0.00 < 0.05)
                        Performance has improved.

bench_play_game_1_000_000
                        time:   [30.260 ns 30.403 ns 30.564 ns]
                        change: [-100.000% -100.000% -100.000%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 4 outliers among 100 measurements (4.00%)
  1 (1.00%) high mild
  3 (3.00%) high severe

哦哇!我們的bench_play_game基準測試回落到原來FizzBuzz測試的附近位置。 我希望我能記住那個得分是多少。但是已經過了三個星期了。 我的終端歷史記錄沒有回溯這麼遠。 而Criterion只會和最近的結果進行比較。 但我認為這是很接近的!

bench_play_game_100基準測試的結果下降了近10倍,-93.950%。 和bench_play_game_1_000_000基準測試的結果下降了超過10,000倍!從9,596,800 ns降到30.403 ns! 我們甚至讓Criterion的改變計數器達到了最大值,它只會達到-100.000%

🐰 嘿,至少我們在性能bug趕到生產環境之前抓住了它… 哦,對了。算了…

在 CI 中捕獲性能回歸

由於我那個小小的性能錯誤,我們的遊戲收到了大量的負面評論,這讓高管們非常不滿。 他們告訴我不要讓這種情況再次發生,而當我詢問如何做到時,他們只是告訴我不要再犯。 我該如何管理這個問題呢‽

幸運的是,我找到了這款叫做 Bencher 的超棒開源工具。 它有一個非常慷慨的免費層,因此我可以在我的個人項目中使用 Bencher Cloud。 而在工作中需要在我們的私有雲內,我已經開始使用 Bencher Self-Hosted

Bencher有一個內建的介面卡, 所以很容易整合到 CI 中。在遵循快速開始指南後, 我能夠運行我的基準測試並用 Bencher 追蹤它們。

$ bencher run --project game "cargo bench"
    Finished bench [optimized] target(s) in 0.07s
     Running unittests src/lib.rs (target/release/deps/game-13f4bad779fbfde4)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/release/deps/game-043972c4132076a9)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running benches/play_game.rs (target/release/deps/play_game-e0857103eb02eb56)
Gnuplot not found, using plotters backend
bench_play_game         time:   [3.0713 µs 3.0902 µs 3.1132 µs]
Found 16 outliers among 100 measurements (16.00%)
  3 (3.00%) high mild
  13 (13.00%) high severe

bench_play_game_100     time:   [23.938 ns 23.970 ns 24.009 ns]
Found 15 outliers among 100 measurements (15.00%)
  5 (5.00%) high mild
  10 (10.00%) high severe

bench_play_game_1_000_000
                        time:   [30.004 ns 30.127 ns 30.279 ns]
Found 5 outliers among 100 measurements (5.00%)
  1 (1.00%) high mild
  4 (4.00%) high severe

Bencher New Report:
...
View results:
- bench_play_game (Latency): https://bencher.dev/console/projects/game/perf?measures=52507e04-ffd9-4021-b141-7d4b9f1e9194&branches=3a27b3ce-225c-4076-af7c-75adbc34ef9a&testbeds=bc05ed88-74c1-430d-b96a-5394fdd18bb0&benchmarks=077449e5-5b45-4c00-bdfb-3a277413180d&start_time=1697224006000&end_time=1699816009000&upper_boundary=true
- bench_play_game_100 (Latency): https://bencher.dev/console/projects/game/perf?measures=52507e04-ffd9-4021-b141-7d4b9f1e9194&branches=3a27b3ce-225c-4076-af7c-75adbc34ef9a&testbeds=bc05ed88-74c1-430d-b96a-5394fdd18bb0&benchmarks=96508869-4fa2-44ac-8e60-b635b83a17b7&start_time=1697224006000&end_time=1699816009000&upper_boundary=true
- bench_play_game_1_000_000 (Latency): https://bencher.dev/console/projects/game/perf?measures=52507e04-ffd9-4021-b141-7d4b9f1e9194&branches=3a27b3ce-225c-4076-af7c-75adbc34ef9a&testbeds=bc05ed88-74c1-430d-b96a-5394fdd18bb0&benchmarks=ff014217-4570-42ea-8813-6ed0284500a4&start_time=1697224006000&end_time=1699816009000&upper_boundary=true

使用這個由一個友善的兔子給我的巧妙的時間旅行裝置, 我能夠回到過去,重演如果我們一直都在使用Bencher的情況下會發生什麼。 你可以看到我們首次推出存在問題的FizzBuzzFibonacci實現的位置。 我馬上在我的拉取請求評論中得到了CI的失敗資訊。 就在那天,我修復了性能問題,擺脫了那不必要的額外循環。 沒有火災。顧客都非常開心。

Bencher: 持續性能基準測試

🐰 Bencher

Bencher是一套持續型的性能基準測試工具。 你是否曾經因為性能回歸影響到了你的使用者? Bencher可以防止這種情況的發生。 Bencher讓你有能力在性能回歸進入生產環境 之前 就進行檢測和預防。

  • 運行: 使用你喜愛的基準測試工具在本地或CI中執行你的基準測試。bencher CLI簡單地包裝了你現有的基準測驗裝置並儲存其結果。
  • 追蹤: 追蹤你的基準測試結果的趨勢。根據源分支、測試床和度量,使用Bencher web控制檯來監視、查詢和繪製結果圖表。
  • 捕獲: 在CI中捕獲性能回歸。Bencher使用最先進的、可定製的分析技術在它們進入生產環境之前就檢測到性能回歸。

基於防止功能回歸的原因,在CI中運行單元測試,我們也應該使用Bencher在CI中運行基準測試以防止性能回歸。性能問題就是錯誤!

開始在CI中捕捉性能回歸 — 免費試用Bencher Cloud

Rust的精髓

https://mp.weixin.qq.com/s?__biz=Mzg4MTYyNDU4Nw==&mid=2247483686&idx=1&sn=4e078a7e273d62fdbda099b908036603&scene=21#wechat_redirect

我嘗試用幾個簡單的詞彙說明Rust的設計精髓和底層原理,方便對比其他語言和Rust的不同之處

•Rust變數具有閱後即焚的特性, 相比之下其他語言的變數都是耐用品, 而Rust的變數屬於一次性用品•Rust語言中變數使用值擁有是明確區分的,而且其他語言的變數等號基本都是賦值,但是Rust是所有權讓渡

三種常見的記憶體模式

從下面三行簡單的賦值語句, 我們直觀感受一下c++,python, 還有Rust的不同處理方式

# c++程式碼,僅僅用來說明簡單邏輯auto s = std::vector<std::string>{ "udon", "ramen", "soba" }; auto t = s;    // 第一次使用sauto u = s;    // 第二次使用s

c++ will copy

圖片c++-copy

棧內的變數一直增長(從左往右),變數也一直可以被訪問使用. 而且棧到堆的指向關係互相交織(網狀); 由於C++默認採用copy的方式進行operator=的操作,即使是std::vector複雜的STL結構,都是直接複製,鑑於這種默認動作開銷比較大,一般程式設計師會手工引入引用或者指針來最佳化(問題隨之而來,棧到堆的指針遲早變成網)

Python will count

圖片python3-count

對於python而言,由於有gc的存在,gc採用了reference count技術,所以邏輯層次上多了一個PyObject的中間層,保存了計數資訊,由於採用的是計數機制,而python棧上的變數都是對同一個值的多個引用,修改其中一個總是會讓其他變數的值也都一起變化

Rust will move (and crush!!)

圖片rust-will-move-and-crush

對於Rust而言,變數的賦值操作等同於值擁有權的讓渡, 它的意義就是auto t = s;這種語句一旦執行,相當於棧變數t取代了棧變數s,擁有了底層的值, 隨之而來的就是s在編譯階段就被編譯器標識為不再可用; 這是rust編譯器處理程式碼的邏輯,所以auto u = s;這樣的語句根本不會通過編譯,更無需再考慮程式碼執行;x 這一切都發生在程式碼分析階段,編譯過程中, 不管是簡單的賦值,還是被函數形參,還是一個值被從函數返回: 都是直接的管理權讓渡

思考題

一個for循環,內部一個print函數列印了上面列表,這樣一個簡單的邏輯不同的語言會出現什麼樣的記憶體結果?

•c++ OK, s可以再次被賦值使用,列印,進行各種操作•Python OK, s也可以正常使用•Rust OK, 可以正常列印,但是for訓話結束之後,所有字串堆中的資源都被釋放了, s變成了不可再用的變數

好了,是不是感覺太神奇了~~, Rust對變數的使用就是直接拿來,如果沒有新的上下文接受讓渡, 變數就被直接銷毀了, 這個神奇的設定就是Rust有別於其他各種語言,並且會有move sematicborrow, lifetime的最底層設定, 這就是rust的遊戲規則.

簡單發散思考, 這個神奇的move sematic設定會導致什麼樣的直觀現象呢,類似與c++,java,python各種語言其實隨著程序一點一點執行,可能會有成百上千的object產生,其中的變數指針,引用,copy互相交織在一起,看起來就會亂糟糟的,這常被稱之為對象之海(如下圖)

圖片a sea of objects

由於rust特立獨行的底層遊戲規則,不管程式執行了多久,邏輯上看來,不管對象內部有多少子元素,列表還是字典,永遠只有一個root(擁有它), 再加上我們將要說到的使用權的限制,Rust的堆疊變數總是非常幹淨清楚(給你了,你就是owner), 你不會有類似c++中三方庫函數返回了一個指針, 我應該free?的疑問.(如下圖)

圖片

30 天深入淺出 Rust系列

https://ithelp.ithome.com.tw/articles/10199503

這系列會假設你有基礎的程式能力,不管是什麼語言都行,至少知道一下比如「參考 (reference) 」是什麼,雖說我也會盡量以初學程式的角度講解,只是 Rust 這個語言本身就包含了不少比較進階的觀念,加上我很可能會舉其它程式語言當對照,同時本系列會以類 Unix (Linux, Mac) 的環境為主,如果你使用的是 Windows 大部份情況下應該不會有什麼問題。

本系列的程式都會在 Ubuntu 18.04 下測試過,如果有任何問題歡迎回報,如果你有任何問題歡迎留言問我,我很樂意解答的,或是你有任何建議也行,我會很高興的。

Rust 是由 Mozilla 所主導的系統程式語言,旨在快速,同時保證記憶體與多執行緒的安全,這代表者使用 Rust 開發基本上不會再看到諸如 Segmentation Fault 等等的記憶體錯誤了,強大的 trait 系統,可以方便的擴充標準函式庫,這讓 Rust 雖然是靜態的程式語言,卻也有極大的靈活性,同時目前也有不少的應用,比如網頁後端、系統程式還有 WebAssembly ,另外也因為其速度快與語法簡潔跟豐富的生態,也有不少公司用來處理極需要速度的部份,比如 Dropbox, npm 想知道還有誰用可以去看看還有誰也在用

預計會花 20 篇左右把 Rust 語言介紹完,剩下的則是來實作一些實際的專案,以及介紹一些 Rust 的套件與生態系, 預計會做的有:

  • 連結 c 函式庫,跟現有的程式碼或第三方函式庫做整合,讓你不用重新造輪子
  • 寫一個 python 的 native extension ,擴充 python 的功能
  • 寫個指令列的程式,介紹使用 clap 做 argument parsing
  • 寫個網頁後端,介紹 Rust 的 ORM diesel 與非同步的 tower-web 後端框架

或是有任何建議,或者你覺得改做什麼樣的專案會更有趣的也歡迎提出。

安裝環境

在正式開始教學前,我們要先把環境安裝好,請在終端機輸入以下指令:

$ curl https://sh.rustup.rs -sSf | sh -- -y

$ 是代表終端機的提示符號,你並不需要輸入這個符號

另外 Rust 需要 GCC 之類的工具,如果您的環境是 Ubuntu,可以直接用以下指令安裝:

$ sudo apt-get install -y build-essential

如果您的環境是 Windows,請至 https://rustup.rs 下載 rustup-init 來安裝,而因本教學會使用到相當多指令,我會建議使用 cmder 來取代原本 Windows 內建的 cmd,或是直接使用 Visual Studio Code(VSCode)。

完成以上步驟後,您的電腦現在應該已安裝了以下程式:

  • rustup: Rust 本身的安裝軟體及版本管理以及更新系統
  • rustc: Rust 的編譯器
  • cargo: Rust 的套件管理器

剛安裝完,為了確保環境變數有正確設定,你需要在終端機執行下方指令:

$ source $HOME/.cargo/env

而在你下次登入後 Rust 應該已經幫你自動設定好環境變數了,你就不需要再執行上面那條指令。

接著,可以使用 rustc --version 來查看已安裝的版本,如有正確顯示目前的版本,就代表安裝好囉,在本文撰寫時穩定版為 1.29.1。

一切都安裝設置好後,不免的,我們來寫個「Hello, World!」吧!開啟喜歡的編輯器並輸入以下程式碼:

fn main() {
    println!("Hello world");
}

這邊的 ! 並不是打錯了, println! 是在一起的,在 Rust 裡以 ! 結尾的東西是巨集 (macro) ,現在只需理解 println! 在背後會幫你產生一些程式碼,讓你可以用簡單的方式就完成印到螢幕這個動作,之後會介紹如何自己寫巨集。

完成後將程式存檔為 hello.rs ,並在終端機執行下方指令:

$ rustc hello.rs

沒意外的話在同一個資料夾下將會看到名稱為 hello 的執行檔(Windows 下為 hello.exe), 在終端機下執行:

Hello world

恭喜您,您已完成了第一個 Rust 程式,從下一篇開始,我們將正式開始旅程,不過在這之前,讓我們安裝幾個好東西,這會讓我們接下來更佳順利。

$ rustup component add rls-preview rustfmt-preview

最後的最後,Rust 有個線上的測試環境:https://play.rust-lang.org ,並且還安裝好了很多常用的套件,如果臨時有什麼想測試的可以直接在這個網站使用。

Rust 的套件管理工具 Cargo 與套件倉庫

這篇要介紹的是 Rust 的套件管理工具 Cargo 以及套件倉庫 crates.io 目前 crates.io 上有一萬八千多個套件,很多功能你都可以在上面找到別人幫你寫好的套件。

在 Rust 要使用別人寫好的套件你需要直接修改 Cargo.toml 這個檔案,把套件的名字以及你需要的版本加進去,但無需擔心,它的寫法很簡單的,以下是個範例:

[package]
name = "guess"
version = "0.1.0"
authors = ["DanSnow"]

[dependencies]
rand = "0.5.5"

Cargo.toml 跟 Node.js 的 package.json 的用途很像。

我們來建立我們的第一個專案吧,請打開終端機輸入以下的指令:

$ cargo init guess

這時你的目錄下應該有個名稱為 guess 的資料夾並且你可以看到底下的檔案與資料夾:

guess
├── Cargo.toml
├── .gitignore
└── src
    └── main.rs

. 開頭的檔案或目錄在類 Unix 系統下是隱藏檔,如果你使用的是 Ubuntu 預設的檔案管理員的話你可以按 Ctrl+H 顯示所有檔案。你也可以使用指令 ls -a 顯示檔案。

其中 src/main.rs 就是我們接下來要修改的專案主程式的程式碼了,你會看到裡面已經有 Hello world, Cargo.toml 也已經幫你填好了基本的設定, 我們現在試著輸入 cargo run 來執行看看:

$ cargo run
   Compiling guess v0.1.0 (guess)
    Finished dev [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/guess`
Hello, world!

這次要做的是個終極密碼的小遊戲,主要遊戲方式是由使用者輸入數字,電腦回答是比答案大還是小,若沒猜中則猜到中為止的遊戲,接下來我們會一邊介紹基本語法一邊完成這個遊戲,不過我們先幫我們的專案加上一個套件吧,打開 Cargo.toml ,找到有一行為 [depeendencies] ,在它底下加上:

rand = "0.5.5"

這個套件的功能是產生隨機的數字,如果沒有它,我們的小遊戲每次的答案就要一樣了。

不過像這樣手動編輯檔案也挺容易出錯的,我們有個更好的方法,請你輸入底下的指令:

$ cargo install cargo-edit

這會幫 cargo 擴充新的功能,現在我們可以用底下的指令來加上套件了:

$ cargo add rand

是不是方便多了?

Rust 基礎

我們直接打開 main.rs 來寫我們的程式吧,首先 // 開頭的是程式的註解,它是給人看的,電腦看到會直接忽略,我直接使用註解來說明程式的內容,希望你可以照著程式碼自己打一遍,這樣做相信會比較有印像,當然,註解的部份可以不用照著打,你也可以用你自己的方式用註解做筆記。請照著底下的內容輸入:

// 宣告使用外部的函式庫 rand
// 這告訴我們的編譯器我們要使用 rand 這個套件
extern crate rand;

// 引入需要的函式,如果不引入的話我們就需要在程式中打出全名來,
// 比如像下面使用到的 thread_rng 的全名就是 rand::thread_rng ,
// 但這裡我們選擇引入 rand::prelude::* 這是個比較方便的寫法,
// 很多套件的作者為了使用者方便,都會提供個叫 prelude 的模組,
// 讓你可以快速的引入必要的函式,我們要使用的 thread_rng 也有包含在裡面,
// 但並不是每個套件作者都會這麼做,請注意。
use rand::prelude::*;

// 這是標準輸入,也就是來自鍵盤的輸入,我們等下要從鍵盤讀玩家的答案。
use std::io::stdin;

// 這是一備函式,函式就是一段的程式,
// 我們可以在一個程式裡根據不同的功能將程式拆成一個個的函式,
// 不過今天這個程式並不大,我們直接全部寫在 main 這個函式裡就好了,
// main 是個特殊的函式, Rust 的程式都會從 main 開始執行。
fn main() {
    // 我們在這定義了一個變數 ans 來當作我們的答案,
    // 將它設定成 1~100 之間的隨機數字
    let ans = thread_rng().gen_range(1, 100);

    // 這邊又定義了兩個變數,分別代表答案所在的上下範圍,
    // 之後我們要把這個範圍做為提示顯示給玩家,
    // 因為之後需要修改這兩個變數的值,所以這邊必須加上 mut 來表示這是可修改的
    let mut upper_bound = 100;
    let mut lower_bound = 1;

    // 這是迴圈,它會重覆的執行包在這裡面的內容,
    // 因為這邊的迴圈沒有條件,所以它會一直反覆的執行,
    // 直到執行到如 break 才會結束,
    // 等下還會介紹另外兩種有條件的迴圈
    loop {
        // 這邊要建一個用來暫放玩家輸入的答案用的變數,
        // String 是個存放一串文字用的型態,也就是字串型態,
        // String::new 會建立一個空的字串
        let mut input = String::new();

        // 這邊要印出提示使用者輸入的顯示,同時我們也印出答案所在的上下界,
        // println! 在印完會自動的換行,也就是接下來的輸入輸出會從下一行開始,
        // 而裡面的 {} 則是用來佔位子用的,分別是我們要印出上下界的位置,
        // 之後傳給 println! 的變數就會被放在這兩個位置
        println!(
            "答案在 {}~{} 之間,請輸入一個數字",
            lower_bound, upper_bound
        );

        // 這邊我們使用 read_line 從鍵盤讀入一整行進來,
        // 也就是到玩家按下 Enter 的字都會讀進來,
        // 讀進來的文字會被放進 input 裡,
        // 而因為放進 input 代表著修改 input 的內容,
        // 所以這邊比較特別一點,我們要加上 &mut 來允許 read_line 修改 input ,
        // 而 read_line 會除了把輸入放進 input 外也會傳回是否有成功讀取輸入,
        // 於是這邊就使用了 expect 來處理,若回傳的值代表錯誤時,
        // expect 會印出使用者傳給它的訊息並結束掉程式
        stdin().read_line(&mut input).expect("Fail to read input");

        // trim() 會把字串前後的空白字元 (空格與換行) 去掉,
        // 而 parse::<i32>() 則是把字串從原本的文字型態轉換成數字,
        // 這樣我們在之後才可以拿它來跟答案做比較,
        // 我們這邊又重新定義了一次 input 來放轉換成數字後的結果,
        // 如果你有學過其它的語言可能會覺得奇怪,為什麼允許這麼做,
        // 這也是 Rust 一個有趣的地方, Rust 允許你重覆使用同一個變數名稱,
        // parse 也是回傳代表正確或錯誤的 Result 不過這次我們不用 expect 了,
        // 這次我們判斷是不是轉換失敗,如果是則代表玩家輸入了不是數字的東西,
        // 那我們就讓玩家再輸入一次, match 是用來比對多個條件的語法,之後
        // 會有一篇來介紹這個語法,因為它是 Rust 裡一個很強大的功能。
        let input = match input.trim().parse::<i32>() {
            // Ok 代表的是正確,同時它會包含我們需要的結果
            // 因此這邊把轉換完的數字拿出來後回傳
            // Rust 裡只要沒有分號,就會是回傳值
            Ok(val) => val,
            // Err 則是錯誤,它會包含一個錯誤訊息,不過我們其實不需要,
            // 這邊我們直接提示使用者要輸入數字並結束這次迴圈的執行
            Err(_) => {
                println!("Please input a number!!!");
                // continue 會直接跳到迴圈的開頭來執行,也就是 loop 的位置
                continue;
            }
        };

        // 這邊使用 if 來判斷玩家的答案跟正確答案是不是一樣,
        // if 會判斷裡面的判斷式成不成立,如果成立就執行裡面的程式,
        // 要注意的是判斷相等是雙等號,因為單個等於已經用在指定了。
        if input == ans {
            println!("恭喜你,你答對了");
            // break 則會直接結束迴圈的執行,
            // 於是我們就可以離開這個會一直跑下去的迴圈
            break;
        // 如果不一樣,而且玩家的答案比正確答案大的話就更新答案的上限
        } else if input > ans {
            upper_bound = input;
        // else 會在 if 的條件不成立時執行,並且可以串接 if 做連續的判斷,
        // 像上面一樣。都不是上面的情況的話就更新下限
        } else {
            lower_bound = input;
        }
    }
}

變數宣告

定義一個變數的語法是像這樣的:

#![allow(unused)]
fn main() {
let ans: i32 = 42;
}

變數是用來存放程式的資料用的,等號在這邊是指定的意思,並不是數學上的相等,我們把 42 指定給 ans 這個變數,而因為 42 是一個數字,因此我們把 ans 這個變數指定為 i32 這個整數型態,然而事實上大部份情況下其實是不需要給型態的,因為 Rust 可以自動的從初始值,也就是你一開始指定的值推導出你的變數的型態。

在 Rust 裡預設變數是不可以修改的,一但給了值就不能改變,如果要能修改就必須加上 mut ,這可以讓你在之後還能修改變數的值。

錯誤的範例:

#![allow(unused)]
fn main() {
let ans = 42;
ans = 123; // 這邊重新指定了值給 ans ,但 ans 並沒有宣告為可改變的
}

正確的範例:

#![allow(unused)]
fn main() {
let mut ans = 42;
ans = 123;
}

在 Rust 中的變數都必須要有初始值,如果沒有初始值會是一個錯誤,當然,你可以先宣告再給值,但只要有可能會發生沒有指定初始值的情況就會發生錯誤。

錯誤的範例:

#![allow(unused)]
fn main() {
let ans;
if true {
    ans = 42;
}
// 這邊我們沒有 else 的情況就使用了,所以 Rust 認為 ans 可能沒有初始值
println!("{}", ans);
}

正確的範例:

#![allow(unused)]
fn main() {
let ans;
if true {
    ans = 42;
} else {
    ans = 0;
}
println!("{}", ans);
}

基本型態

在電腦裡資料都有其型態,電腦必須知道資料屬於哪一種才能做出正確的處理,而 Rust 的基本型態則有:

以下為了說明都有在定義變數時把型態寫出來,但平常因為可以自動推導,所以並沒有必要寫出來。

  • unit: 型態是 () ,同時值也是 () ,這是一個代表「無」的型態
#![allow(unused)]
fn main() {
//  就是「無」,你沒辦法拿這個值來做什麼
let unit: () = ();
}
  • 布林值 bool: 值只有真 true 與假 false ,一個代表真假的型態,同時也是判斷式所回傳的型態
#![allow(unused)]
fn main() {
let t: bool = true;

// if 判斷的結果一定要是布林值,其實所有的判斷式的結果也都是布林值
if t {
    println!("這行會印出來");
}

if false {
    println!("這行不會印出來");
}
}
  • 整數: 上面使用的 i32 就是整數,實際上整數家族有具有正負號的 i8i16i32i64i128 與只能有正整數的的 u8u16u32u64u128 它們之間的差別在有沒有正負號,以及能存的數字最大的大小,平常通常只會用到 i32 ,先只要記得 i32 就行了
#![allow(unused)]
fn main() {
let num: i32 = 123;
// 數字你可以做基本的四則運算:加法 (+) 、減法 (-) 、乘法 (*) 、除法 (/)
// 和 取餘數 (%) ,當然也還有比如取絕對值之類的方法在,不過這邊先不提
println!("1 + 1 = {}", 1 + 1);
println!("10 % 3 = {}", 10 % 3);
}
  • 浮點數: 就是小數,有 f32f64 ,但平常只會用到 f64 ,浮點數也可以像整數一樣做計算,只是無法取餘數
#![allow(unused)]
fn main() {
let pi: f64 = 3.14;
}
  • 字串: String ,代表的是一串的文字,另外還有切片型態的 str (這兩個詳細的差別之後再提) ,如果你要讀使用者的輸入或是要能夠修改內容的要用 String ,如果要放固定的字串 (比如顯示的訊息) 用 str
#![allow(unused)]
fn main() {
let message: &str = "這是個固定的訊息";
println!("{}", message);
let mut s: String = String::from("可以用 from 來建一個 String");
s.push_str(",同時你也可以增加內容在 String 的結尾");
println!("{}", s);
}
  • 字元: char 一個字就是一個字元,比如 'a' 或是 '三' 也是
#![allow(unused)]
fn main() {
let c1: char = '中';
let c2: char = '文';
let c3: char = '也';
let c4: char = '行';
}

這邊為了說明都有在變數後加上型態,如果平常沒加型態而是讓編譯器自動推導的話,整數預設會使用 i32 ,符點數會使用 f64 ,但若之後把變數傳入了其它函式,則會配合那個函式的需求改變。

複合型態

  • 陣列 (array) :由一串相同型態與固定長度的資料組成的
#![allow(unused)]
fn main() {
// i32 是元素的型態, 4 則是長度
let mut array: [i32; 4] = [1, 2, 3, 4];

// 這邊印出第一個數字與最後一個數字,陣列的編號是從 0 開始的
println!("{}, {}", array[0], array[3]);

// 我們也可以修改元素的值,同樣的,請注意上面的定義是有加 mut 的
array[1] = 42;
}
  • 元組 (tuple):可由不同的型態組成
#![allow(unused)]
fn main() {
// 元組的型態要把每個欄位的型態都寫出來
let mut tuple: (i32, char) = (42, 'a');

// 印出第一個值
println!("{}", tuple.0);

// 改變第二個值
tuple.1 = 'c';
}

條件判斷 if

#![allow(unused)]
fn main() {
// if 裡面要放條件判斷,或是布林值,
// 條件判斷除了上面出現的 == 、 > 、 < 外還有個不等於
if 10 != 5 {
    println!("10 != 5");
}
}

如果要判斷多個條件可以用底下的方式串起來:

  • && :「且」,兩邊都成立時才成立
  • || :「或」,其中一邊成立就成立
#![allow(unused)]
fn main() {
if 2 > 1 && 3 > 2 {
    println!("2 > 1 且 3 > 2");
}
}

還有 ! 可以把判斷式的結果反轉,也就是把布林值的 truefalsefalsetrue

#![allow(unused)]
fn main() {
if !false {
    println!("這行會印出來");
}
}

另外你可以用 if 來根據條件來指定變數:

#![allow(unused)]
fn main() {
let ans = if true {
// 請注意,這邊沒有分號
    42
} else {
    123
}; //  這邊要加分號
}

Rust 裡只要不加分號就會變回傳值

while

while 是有條件的迴圈,只有當條件滿足時才會繼續執行下去:

#![allow(unused)]
fn main() {
let mut i = 0;
while i < 5 {
    println!("i = {}", i);
    // 這是 i = i + 1; 的縮寫,數學運算都可以這樣寫
    if i == 3 {
        // 我們在 i 為 3 時就結束迴圈了,所以你會看到從 0 印到 3
        break;
    }
    i += 1;
}
}

break 可以用來中斷迴圈,而 continue 可以直接結束這次的迴圈,跳到迴圈的開頭執行。

for

for 是用來跑過一個「範圍」的資料的,比如像陣列,以後會再詳細介紹背後的機制。

#![allow(unused)]
fn main() {
for item in [1, 2, 3, 4, 5].iter() {
    println!("{}", item);
}

for item in 0..5 {
    println!("{}", item);
}
}

其中 0..5 是 Rust 的 range 代表的是 0~4 (不含結尾),含結尾的話要寫成 0..=5 (有等號),這代表一個數字的範圍,以後講到切片時會再提到。

函式

上面說了函式就是一小段的程式,如果你的程式裡出現了重覆的程式碼,你可以試著把程式碼抽出來變成函式,這樣以後要修改也會比較方便。

函式來自於數學的函數的觀念「給予一個值,會對應到另一個固定的值」,函式同樣的也需要輸入的值與輸出的值,底下是個範例:

// 這個函式有兩個整數的輸入值 a 與 b 並且回傳一個整數
// 函式的開頭是 fn 接下來跟著函式的名字,後面的括號裡放著函式的輸入
// 其中當作輸入的鑾數都一定要有型態,之後 -> 後放著的是回傳的型態
fn add(a: i32, b: i32) -> i32 {
    // 這邊示範如何使用 return ,如果 a 與 b 都是 1 ,就直接回傳 2
    if a == 1 && b == 1 {
        // return 會提早結束函式的執行,並且把後面的值當成回傳值
        return 2;
    }
    // 注意這邊沒有括號,沒有括號的代表回傳值,當然你也可以像上面使用 return
    a + b
}

// 這個函式沒有寫出回傳值,這代表它其實會回傳一個 () ,只是可以省略不寫出來而已
fn print_number(num: i32) {
    println!("{}", num);
}

// 所以其實 main 函式回傳的也是 unit
fn main() {
    // 像這樣子就可以呼叫函式了
    let num = add(1, 2);
    print_number(num);

    // 你也可以寫在一起
    print_number(add(1, 2));
}

這篇我們很快的介紹了 Rust 的基本語法,下一篇要介紹的是 Rust 的參考,以及 Rust 中很重要的變數的所有權的觀念。

變數的所有權與借出變數

Move, Borrow & Ownership

這篇與下一篇要介紹 Rust 中可說是最複雜,卻也是最重要的一個觀念,變數的所有權 (ownership) ,在 Rust 中每個變數都有其所屬的範圍 (scope) ,在變數的有效的範圍中,可以選擇將變數「借 (borrow)」給其它的 scope ,也可以將所有權整個轉移 (move) 出去,送給別人喔,當然,送出去的東西如果別人不還你的話是拿不回來的,但借出去的就只是暫時的給別人使用而已。

Move

fn main() {
  let message = String::from("Hello");
  {
    message;
  }
  println!("{}", message);
}

Playground

範例的下方若有個 Playground 的連結,按下去就會連到 Rust Playground ,讓你可以直接執行範例。

補充一個之前忘了提的東西, ! 在這邊並不是打錯了, println! 是一起的

fn greet(message: String) {
  println!("{}", message);
}

fn main() {
  let message = String::from("Hello");
  greet(message);
  println!("{}", message);
}

猜看看上面的兩段程式碼的執行結果是什麼,猜到了嗎,答案都是無法編譯,編譯器會出現:

error[E0382]: use of moved value: `message`

意思是使用了已經送給別人的變數,在 Rust 中一個程式碼的區塊, 也就是由 {} 包圍的區域都是一個 scope ,這也包含了函式、迴圈的括號等等,只要你把變數傳給了其它區塊,都會把變數送出去,所以在上面的範例中, message 這個變數已經送出去,並且在接下來的 println! 無法使用了,另外在底下的情況也會送出變數:

#![allow(unused)]
fn main() {
let a = String::from("a");
let b = a;
println!("{}", a); // 這邊也同樣不能編譯
}

或許你已經注意到了,這邊使用的都是 String::from ,都是在建立字串,如果把上面的例子都換成數字的話,你會發現不會出現任何錯誤,而能順利的執行,因為數字可以 複製 ,字串不能複製嗎?也可以,只是字串的大小並不固定,有可能是很長的一篇文章,也有可能是一個空字串, Rust 並不允許在沒有明確的說要複製的情況下複製這種不知道會花費多少成本的型態,如果要改寫上面的範例,複製一個字串的話,可以使用 clone

#![allow(unused)]
fn main() {
let a = String::from("a");
let b = a.clone();
println!("{}", a);
}

Playground

數字的大小則是固定的,於是在發生把變數送出去的情況時, Rust 會使用複製一份的方式給別人,所以就變成了兩個人都擁有,不會發生錯誤的情況。

如果你想知道哪個型態可以被複製,可以參考文件的 std::marker::Copy ,你會在底下看到如 impl Copy for i32 這就代表 i32 可以被複製

拿走的東西主動的還回去也是可以的:

// 我要拿走整個 message 變數
fn greet(message: String) -> String {
  println!("{}", message);
  message // 之後再還回去
}

fn main() {
  let message = String::from("Hello");

  // 這邊變數被拿走了,但是又還了回來,於是我們需要一個變數代表它
  // 當然你也可以使用同樣的名稱 message
  let msg = greet(message);

  println!("{}", msg); // 又拿回來了,於是可以使用
}

Playground

Borrow

Rust 中把出借變數直接稱為 borrow , Rust 中使用在變數前面加一個 & 來代表出借變數,borrow 的用途是當你不想把變數送出去時,你就可以把你的變數 出去,但還有個前提是對方要 願意跟你借 ,底下是個借出變數給函式的範例:

// 這邊在 String 的前面加上了 & 代表我可以跟別人用借的
fn greet(message: &String) {
  println!("{}", message);
}

fn main() {
  let message = String::from("Hello");
  greet(&message); // 這邊加上了 & 來表示借出去
  println!("{}", message); // 借出去的東西只是暫時給別人而已,自己還可以使用
}

Playground

// 這邊沒有加上 & 代表我想要整個拿走
fn greet(message: String) {
  println!("{}", message);
}

fn main() {
  let message = String::from("Hello");
  // greet(&message); // 這邊就算加上了 & 也沒辦法把變數用借的借出去
  greet(message); // 一定要整個給它
  // println!("{}", message); // 因為被整個拿走了,所以這邊已經沒辦法使用了
}

Rust 預設借給別人的東西別人必須原封不動的還回來,也就是借出去的變數是沒辦法被修改的,如果你想允許別人修改的話,你就必須使用 &mut 對方也必須明確的使用 &mut 來代表我要借到一個可以修改的變數:

fn combine_string(target: &mut String, source: &String) {
  // push_str 會把傳進去的字串接到字串的後面
  target.push_str(source);
}

fn main() {
  // 這邊一定要加 mut ,因為這個變數會被修改,就算不是你自己改的也一樣
  let mut message = String::from("Hello, ");
  let world = String::from("World");
  // 借給 combine_string 一個可以改的變數 message ,與一個不能改的 world
  combine_string(&mut message, &world);
  println!("{}", message); // 這邊就會印出 Hello, World
}

Playground

還記得前一篇的猜數字裡有 stdin().read_line(&mut input) 嗎?

Borrow 的規則

Rust 的出借變數是有其規則在的:

  1. 所有的變數一次都只能用可以修改的方式 (&mut) 出借一次
#![allow(unused)]
fn main() {
let mut n = 42;
let a = &mut n;
let b = &mut n; // 這裡用可以修改的方式總共借出去兩次了,這是不可以的
}
  1. 可以無限的用唯讀的方式借出去
#![allow(unused)]
fn main() {
let n = 42;
let a = &n;
let b = &n;
}
  1. 一旦用可以修改的方式 (&mut) 出借,那你就不能用任何其它的方式存取變數了
#![allow(unused)]
fn main() {
let mut n = 42;
{
  let a = &mut n;
  // println!("{}", n); // 你不可以使用原本的 n
  // let b = &n; // 你也不可以再用任何方式借走 n
}
println!("{}", n); // 我們離開了 a 借走 n 的範圍了,於是 n 又可以用了
}
  1. 一旦你用唯讀的方式借出了變數,你就不可以修改變數
#![allow(unused)]
fn main() {
let mut n = 42;
{
  let a = &n;
  // n = 123; // 又不可以了,有夠煩的(X
}
n = 123; // 這邊才可以修改
}

這些規則是用來確保多執行緒時不會有資料競爭用的,也就是同時有兩個人修改了同一個變數,於是一次只允許有一個變數的擁有者能修改變數的值,同時一但借出了變數就不能隨意修改,因為別人不一定會知道變數被修改了。雖然有點麻煩 (也真的很麻煩) ,但往好處想,變數不再會被隨意的修改了。

有點可惜的是目前的 borrow checker ,也就是檢查,並執行上面這些規則的功能,它並不是很完善,比如:

#![allow(unused)]
fn main() {
let mut array = [123, 456];
let a = &mut array[0];
let b = &mut array[1];
}

兩個變數分別借走了不相干的兩個部份,但這沒辦法通過檢查,不過這在 Rust 2018 將會有所改善,敬請期待。

Q: Rust 2018 是什麼? A: 在今年的年底 Rust 將要推出 2018 年版,版本號會是 1.30 ,將會有不少的改進以及部份的語法的變更。 Q: 什麼!那我現在學的這些東西到年底就都沒辦法用了? A: 放心好了,大部份的是功能的增強與新的語法,只有一小部份的修改,之後會有一篇來討論這些修改,與看看有哪些新功能。 Q: 那我不想更新可以嗎? A: 可以,你可以設定使用現在的語法版本,也就是 Rust 2015 版。 Q: 那我要怎麼設定? A: 這個之後再說。

String & str, Array & Slice

我們之前應該有提到過 Rust 有兩種字串 Stringstr ,可是一直沒有詳細說明這兩個的差別,這邊我們要提到 Rust 的一個東西「切片 (slice)」,切片可以理解為一次出借如陣列或字串這類的連續的資料型態的一部份:

如果你有寫過 Python 你可能知道 Python 的切片 array[1:3] ,只是這邊把 : 換成了 .. 而已。

#![allow(unused)]
fn main() {
let mut array = [0, 1, 2, 3, 4, 5];
{ // 建立一個區塊,不然我們等下沒辦法使用原本的 array
  let slice: &mut [i32] = &mut array[1..3]; // 這邊一次的借走了 array 的第 2 跟第 3 個元素

  // 然後我們修改了切片的第 1 個元素,對應到原本的 array 則是第二個元素
  slice[0] = 42;

  println!("{:?}", slice); // 會印出 [42, 2]
}
println!("{:?}", array); // 印出 [0, 42, 2, 3, 4, 5]
}

Rust 的切片會知道自己借走了多少長度的東西,而且跟原本的變數 會共用同一塊空間 ,建立切片是不會複製任何資料的。

你可以看到這邊的印出來的結果很明顯的修改了原本的資料,同時很重要的一點,切片 只能有 borrow 的型態 ,因為切片的本質就是出借資料,切片能把資料出借一小段,而使用者可以把這段資料當成像陣列一樣使用。

{:?} 是把資料以 debug 的方式印出來,內建的型態不一定能直接印出來,但大部份都能用這種方式印出來,如果不能使用 {} 印出來時 {:?} 通常能派上用場。

上面的 slice 的型態是 &mut [i32] ,這就是切片型態的寫法,一般如果需要借走一個陣列都會使用切片型態,這樣可以給予使用者更大的彈性,比如決定要不要把整個陣列都借出去,或是隻借出一部份。

那終於可以來講 str 了, str 事實上就是字串的切片,而 String 則是一個可以在執行時改變大小的字串:

#![allow(unused)]
fn main() {
// 直接使用雙引號 (") 的字串都是字串的切片,它們都被 Rust 放在某個地方並且借給使用者使用而已
let hello: &str = "Hello";
// 建立一個 String
let string: String = String::from(hello);
// 借走字串的一部份,產生一個字串切片
let part_of_string: &str = &string[1..3];
}

同樣的 str 也只能有 borrow 的型態。

Lifetime: Borrow 的存活時間

Rust 有個重要的功能叫 borrow checker ,它除了檢查在上一篇提到的規則外,還檢查使用者會不會使用到懸空參照 (dangling reference) ,懸空參照是在電腦世界中一種現象: 如果你今天把一個變數借給別人,實際上借走的人只是知道我可以去哪裡找到這個別人借我的東西而已,那個東西的擁有者還是你本人,以現實世界做比喻的話,這像是借別人東西只是把放那個東西的儲物櫃位置,以及鑰匙暫時的交給別人而已,送別人東西則是直接把儲物櫃的擁有者變成他。

所以如果今天發生了一種情況,你把東西借給別人後,管理每個儲物櫃擁有者的系統馬上把你的使用權收回去呢?會發生什麼事,這沒人說的準,可能儲物櫃還沒被清空,你還是可以拿到借來的東西,或是馬上又換了主人,你已經不是拿到原本的東西了,就像以下的程式碼:

#![allow(unused)]
fn main() {
fn foo() ->&i32 {
  // 這個變數在離開這個範圍後就消失了
  let a = 42;
  // 但是這邊卻回傳了 borrow
  &a
}
}

上面這段 code 是無法編譯的。

為瞭解決這樣的一個問題, Rust 提出來的就是 lifetime 的觀念,只要函式的參數或回傳值有 borrow 出現,使用者就要幫 borrow 標上 lifetime ,標記後讓編譯器可以去追蹤每個變數借出去與釋放掉的情況,確保不會有釋放掉已經出借的變數的可能性。

Rust 使用 'a 一個單引號加上一個識別字當作 lifetime 的標記,所以這些都是可以的 'b, 'foo, '_bar ,此外有兩個保留用作特殊用途的 lifetime: 'static'_

  • 'static: 這代表這是個整個程式都有效的 borrow 比如字串常數 "foo" 它的 lifetime 就是 'static
  • '_:這是保留給 Rust 2018 使用的,這裡先不提它的功能

這邊是個加上 lifetime 標記後的範例:

#![allow(unused)]
fn main() {
fn foo<'a>(a: &'a i32) -> &'a i32 {
  a
}
}

其中我們必須在函式名稱後加上 <> 並在其中宣告我們的 lifetime ,接著把 borrow 的 & 後都加上我們的 lifetime 標記,但事實上在上一篇文章中,我們完全沒用使用到 lifetime , Rust 可以在某些情況下自動推導出正確的 lifetime ,使得實際上需要手動標註的情況並不多,最有可能遇到的情況是一個函式同時使用了兩個 borrow :

fn max<'a>(a: &'a i32, b: &'a i32) -> &'a i32 {
  if a > b {
    a
  } else {
    b
  }
}

fn main() {
  let a = 3;
  let m = &a;
  {
    let b = 2;
    let n = &b;
    // 對於 max 來說, m 與 n 同時存活的這個範圍就是 'a ,
    // 而回傳值也可以在這個範圍內使用
    println!("{}", max(m, n));
  } // b 與 n 會在這邊消失
} // a 與 m 會在這邊消失

這種情況編譯器因為看到了兩個 borrow ,於是沒辦法猜出來回傳的值應該要跟哪個 lifetime 一樣,這邊的作法就是全部都標記一樣的 lifetime ,讓 Rust 知道說我們的變數都會存活在同一個範圍內,同時回傳值也可以在同樣的範圍存活。

大部份的情況下編譯器都能自動的推導,所以需要手動標註的情況其實不多,通常是先嘗試讓編譯器做推導,如果編譯器報錯了才來想辦法標註。

lifetime 還有個用途是用來限制使用者傳入的參數必須是常數:

#![allow(unused)]
fn main() {
fn print_message(message: &'static str) {
  println!("{}", message);
}
}

這個函式就只能接受如 "Hello" 這樣的常數了,雖說只是偶爾會有這樣的需求。

Lifetime Elision (Lifetime 省略規則) (進階)

這部份大概的瞭解一下就好了

  1. 所有的 borrow 都會自動的分配一個 lifetime
#![allow(unused)]
fn main() {
fn foo(a: &i32, b: &i32);
fn foo<'a, 'b>(a: &'a i32, b: &'b i32); // 推導結果
}
  1. 如果函式只有一個 borrow 的參數,則它的 lifetime 會自動被應用到回傳值上
#![allow(unused)]
fn main() {
fn foo(a: &i32);
fn foo<'a>(a: &'a i32) -> &'a i32; // 推導結果
}
  1. 如果有多個 borrow ,但其中一個是 self ,則 self 的 lifetime 會被應用在回傳值
#![allow(unused)]
fn main() {
impl Foo {
  fn method(&self, a: &i32) -> &Self {
  }
}

// 推導結果
impl Foo {
  fn method<'a, 'b>(&'a self, b: &'b i32) -> &'a Self {
  }
}
}

若不符合上面任一條規則,則必須要標註型態。

如果我們把以上的規則套用在上面的範例 max 上:

#![allow(unused)]
fn main() {
fn max(a: &i32, b: &i32) -> &i32 {
  if a > b {
    a
  } else {
    b
  }
}
}

套用規則 1 :

#![allow(unused)]
fn main() {
fn max<'a, 'b>(a: &'a, i32, b: &'b i32) -> &i32 {
  if a > b {
    a
  } else {
    b
  }
}
}

到這邊結束,編譯器已經沒有可用的規則了,但是回傳值的 lifetime 依然是未知,於是就編譯失敗。

Struct 與 OOP

各位有在 C 裡實作過物件導向程式設計 (OOP) 嗎? 這篇要來介紹 Rust 中的 struct 以及 OOP。

Struct

首先來介紹一下結構 (structure) , Rust 中宣告結構是用 struct 關鍵字,這跟 C/C++ 很像,不過不太一樣的是, Rust 中預設所有的成員 (member) 都是私有的 (private)。

#![allow(unused)]
fn main() {
struct Foo {
  pub public_member: i32,
  private_member: i32, // 這個結尾的逗號可加可不加,但是加上去是官方推薦的
}
}

結構讓你可以組合不同的型態,給每個欄位名字,最後組合出一個新的型態,在上面我們宣告了一個有公開的 public_member 與私有的 private_member 的 struct ,事不遲疑,我們寫個 main 來試用一下:

fn main() {
  let foo = Foo {
    public_member: 123,
    private_member: 456
  };
}

Rust 裡建立一個結構就像這樣,在前面放上結構名稱,後面則是各個欄位以及它們的初始值,如果你有接觸過其它的 OOP 程式語言,你應該有注意到哪裡不太對勁了,為什麼我們可以直接使用應該是私有的 private_member?這不是違反封裝了嗎?在 Rust 中私有的成員代表的是隻能在這個 package 中存取 (你可以先理解為這個檔案,之後會詳細解釋 Rust 的模組架構) ,這就像 Java 的 package 修飾或 Kotlin 的 internal 或 C 中使用 static 的全域變數。

封裝是把資料對外部隱藏起來,只允許使用方法存取,避免外部直接存取資料而導致程式的邏輯錯誤

More Struct

如果欄位的名稱不重要,或是隻是想建一個型態把其它的型態包裝起來的話 我們可以建一個 "tuple struct"

#![allow(unused)]
fn main() {
struct Foo(i32, i32);
}

這像 tuple 與 struct 的融合體,如果要存取資料的話也跟 tuple 一樣:

#![allow(unused)]
fn main() {
let foo = Foo(1, 2);
foo.0;
foo.1;
}

另外還有完全沒有資料的 "unit-like struct":

#![allow(unused)]
fn main() {
struct Bar;
}

這個比較少用到,通常是配合 trait 來使用的。

Impl

OOP 是把資料與操作資料的方法結合,藉此模擬現實的東西,於是少不了的,有了資料自然要有操作資料的方法。

struct Person {
  name: String
}

impl Person {
  pub fn new(name: String) -> Self {
    Person { name }
  }

  pub fn say_hello(&self) {
    println!("Hello, my name is {}", self.name);
  }
}

fn main() {
  let john = Person::new("John".to_string());
  john.say_hello();
}

Rust 使用一個分開的 impl 區塊來幫 struct 實作方法,這邊建立了一個 Person 的 struct ,與建構子 new 還有一個方法 say_hello,方法可以設定可見度,在前面加上 pub 就會變公開的方法了,在 Rust 中並沒有指定建構子的名字,所以稱為 new 只是一個慣例,因為 Rust 不允許多型 (Polymorphism),也不允許函式的參數有預設值,所以在 Rust 中一個 struct 有多個不同的建構子也是常有的事,建構子所回傳的 Self 是一個特殊的型態,代表的是自己所實作的 struct ,在這邊你也可以寫 Person,不過如果要回傳自身都建議寫 Self ,因為它有一些好用的特性,在下一章會提到。

其中使用的 Person { name } 是個語法糖,當變數名稱與欄位名稱一致時,就可以把 Person { name: name } 簡寫。

若實作的 method 沒有 self 變數則代表這個方法是個 associated method ,它只是個跟 struct 搭配的方法而已,就像其它語言的 static method ,使用時要用 Person::new 來呼叫。

多型是指函式可以有同一個名稱,但使用不同的參數,這在 OOP 也是很常見的一個特性,比如在 C++ 可以建立 Persion()Persion(string) 兩個建構子,但接受不同的參數

say_hello 則是 Persion 的方法,而 self 是個特殊的變數,只會在實作時出現 它代表的是自己,如果要變成 struct 的方法則第一個參數必須是 self,同時宣告 self 時要決定使用什麼方式宣告,一般最常用的是唯讀的 borrow &self 這代表你會用唯讀的方式存取自己,另外還有 &mut self 這時候你就可以修改自己了,最後 self 這個比較少用, 若使用 self 代表你將會取得自身的所有權,呼叫這個方法將不再能存取這個 struct ,一般用在要將自身轉換成另一個型態,或是呼叫這個方法後自己就不再有效的情況。

大部份情況下還是使用 &self 居多,呼叫方法則是用 john.say_hello() ,Rust 在這邊會自動的把 struct 轉成 borrow 的型態。

Rust 中的所有的 function, method 都可以直接的存取與當成參數傳遞,比如在上面的例子,你可以使用 Person::say_hello(&john) 來呼叫方法,這其實有一種像在 C 中實作 OOP 的感覺,而上面的呼叫方式反而像是語法糖一樣。

建造模式 Builder Pattern

Rust 不支援多型,也不支援預設參數,所以當建構子的參數多時就顯的很麻煩,把所有成員都設成公開的又破壞封裝,所以在 Rust 中常會看到建造模式,建造模式是個設計模式,其將會使用另一個 struct 以一系列的方法來建造我們要的目標,以 Rust 的標準函式庫中的 std::process::Command 為例:

#![allow(unused)]
fn main() {
let output = Command::new("echo")
  .arg("Hello world")
  .output();
println!("{}", String::from_utf8_lossy(&output.stdout));
}

像這樣的方式就是建造模式,最後我們要拿到的目標是那個 output 變數,底下我們自己實做一個:

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
    z: i32,
}

impl Point {
    fn new(x: i32, y: i32, z: i32) -> Self {
        Point { x, y, z }
    }
}

struct Builder {
  x: i32,
  y: i32,
  z: i32,
}

impl Builder {
  fn new(x: i32) -> Self {
    Builder {
      x,
      y: 0,
      z: 0,
    }
  }

  fn y(&mut self, y: i32) -> &mut Self {
    self.y = y;
    self
  }

  fn z(&mut self, z: i32) -> &mut Self {
    self.z = z;
    self
  }
  
  fn build(&self) -> Point {
      Point::new(self.x, self.y, self.z)
  }
}

fn main() {
    let point = Builder::new(1)
        .y(2)
        .z(3)
        .build();
    println!("{:?}", point);
}

Playground

這邊假設了 x 的值是必要的,另外兩個都可以用 0 當預設值,這樣就可以只指定想要的東西了。

Trait 與泛型 (Generic)

Trait

Trait 本身同時提供兩個功能,一是讓不同的型態去實作同樣的功能,再來就是提供實作來共用程式碼了,這同時也是 Rust 泛型的基礎。

#![allow(unused)]
fn main() {
trait Movable {
  fn move(&self);
}

struct Human;

impl Movable for Human {
  fn move(&self) {
    println!("Human walk");
  }
}

struct Rabbit;

impl Movable for Rabbit {
  fn move(&self) {
    println!("Rabbit jump");
  }
}
}

於是不同的型別就能各自實作 trait 並提供自己專屬的實作,另外要注意的是: trait 的方法一定都是公開的。

trait 也可以提供預設的實作,與在使用者實作了特定的方法後提供像 mixin 的功能:

#![allow(unused)]
fn main() {
trait Greeter {
  fn greet(&self) {
    println!("{}", self.message());
  }

  fn greet_to(&self, name: &str) {
    println!("{} {}", self.message(), name);
  }

  fn message(&self) -> &'static str;
}

struct Someone;

impl Greeter for Someone {
  // 提供必要的方法
  fn message(&self) -> &'static str {
    "Hello"
  }

  // 覆寫 (override) 預設實作
  fn greet_to(&self, name: &str) {
    println!("Yo {}", name);
  }
}
}

也可以指定要實作這個 trait 的同時要實作另一個 trait:

#![allow(unused)]
fn main() {
trait HasName: Greeter {
    fn name(&self) -> &'static str;

    fn greet_with_name(&self) {
        println!("{} my name is {}", self.message(), self.name());
    }
}
}

trait 中也可以宣告型態別名 (type alias),這樣就能讓方法能輸入或回傳不同型態:

#![allow(unused)]
fn main() {
trait Foo {
  type Item;
  fn foo(&self) -> Self::Item;
}

struct Bar;

impl Foo for Bar {
  type Item = i32;

  fn foo(&self) -> Self::Item { 42 }
}
}

另外這邊我們都使用 Self ,因為你無法知道是誰會實作這個 trait。

內建的 Trait

Rust 內建了很多的 trait ,只要實作了這些 trait 就能讓 Rust 知道你的型態能提供哪些功能,也能被標準函式庫或第三方的函式庫使用了,以下會介紹幾個比較重要的。

Display

std::fmt::Display 是讓你的型態能被 println! 印出來

#![allow(unused)]
fn main() {
struct Point(i32, i32);

// 當然這邊你可以先用 use std::fmt::Display; 這樣這邊就只需要使用 Display
impl std::fmt::Display for Point {
  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
    write!(f, "({}, {})", self.0, self.1)
  }
}
}

實作了這個 trait 還會自動實作 std::string::ToString ,這是讓你的型態能轉換成字串。

From

std::convert::From 代表你的型態能從另一個型態轉換,之前所使用的 String::from 就是從這裡來的,同時若你實作了 From ,編譯器就會自動幫你實作 IntoInto 則是這個型態可以被轉換成某個固定的型態。

#![allow(unused)]
fn main() {
struct Foo;
struct Bar;

// Foo 是來源的型態,這是等下要講的泛型
impl From<Foo> for Bar {
  fn from(_: Foo) -> Self {
    Bar
  }
}

// 相對的你可以使用 let bar: Bar = Foo.into();
// 這裡也是少數要標記型態的,因為編譯器沒辦法自動推導
}

Add

std::ops::Add 可以讓你的型態與別的東西做加法運算,同時這也是 Rust 的運算子重載, Rust 的所有運算子都有個 trait 在 std::ops,只要實作了你就能使用那個運算子做運算了。

(這邊不提供範例,請去看文件裡的範例)

Deref

std::ops::Deref 這是 Rust 裡一個很重要的運算子,就是取值的操作,只是這個取值也可以取得其它的型態,這代表著你可以用自己定義的型態去包裝不是由你建立的型態,並擴充它的功能,同時還能自動的「繼承」原先的型態所擁有的方法。

這邊的繼承並不像其它語言的繼承,它只是在呼叫方法時透過 Deref 轉換成需要的型態而已。

use std::{ops::Deref, fmt};

#[derive(Copy, Clone)]
struct Num(i32);

impl fmt::Display for Num {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // 直接呼叫被包裝的 i32 所實作的 fmt::Display
        fmt::Display::fmt(&self.0, f)
    }
}

impl Num {
  fn inc_one(self) -> Self {
    Num(self.0 + 1)
  }
}

impl Deref for Num {
  type Target = i32;

  fn deref(&self) -> &Self::Target {
    &self.0
  }
}

fn main() {
  let n = Num(42);
  println!("{}", n.inc_one()); // n 可以有新定義的方法
  println!("{}", n.abs()); // n 也可以有原本定義的方法
}

Derivable Trait

Derivable trait 是一些能自動 產生 實作的 trait ,如果要讓編譯器產生實作的話,就只要在你的型態上加上 derive 的標記:

#![allow(unused)]
fn main() {
#[derive(Debug, Default, Clone, Copy)]
struct Foo;
}

請注意,它們是 產生 實作,這代表它們還是要有程式碼來實作,只是能自動產生而已,通常而言 derivable trait 會要求你的 struct 中每個欄位也都要實作同樣的 trait ,這樣才能遞迴下去

也有不少內建的這樣的 trait ,以下也是介紹重要的

Default

std::default::Default 代表你的型態有預設值,Rust 裡的慣例也有如果實作了無參數的建構子,則也要實作 Default ,你也可以在建構子使用實作的 Default ,另外 Default 也還有個用途:

#![allow(unused)]
fn main() {
#[derive(Default)]
struct Point { x: i32, y: i32, z: i32 };

Point {
  x: 1,
  ..Default::default() // 剩下的值直接使用預設值
}
}

Debug

std::fmt::Debug 是用來印出 debug 資訊的,也就是 println! 使用 {:?} 印出來的結果。

Copy

Copy 是個 marker trait ,這類的 trait 其實並沒有任何實作,它們的用途是讓編譯器知道這個型態的一些特性,以及在什麼情況下該怎麼處理。

Copy 是代表這個型態可以被簡單的複製,這通常代表你的型態裡只有包含像數字或是布林等型態的資料,如果包含了 StringVec 就沒辦法實作這個 trait,另外如果有實作 Copy 則一定要實作 Clone

Clone

std::clone::Clone 是可被複製的型態,如果一個型態只有 Clone 而沒有 Copy 則通常代表這個型態的複製是需要成本的,比如 String,大部份的型態也都有實作 Clone ,如果你的型態允許複製也請務必實作 Clone,至於沒有實作 Clone 的型態基本上就是像 File 之類的因為它是對應到了一個實際存在的檔案。

泛型

如果沒有泛型實際上 trait 也沒什麼作用,泛型可以讓一個函式接受不同型態的參數,同時透過指定要實作的 trait 來確保傳進來的參數一定滿足某些必要的條件,比如我想要傳進來的數字可以跟數字相加,而且回傳數字:

#![allow(unused)]
fn main() {
use std::ops::Add;

fn print_add_one<T: Add<i32, Output = i32>>(n: T) {
  println!("{}", n + 1);
}
}

Add 本身也是一個泛型的 trait 它的參數是用泛型,並且還帶有一個型態別名,我們可以在 <> 中指定泛型的參數,以及型態的別名,第一個 i32 指定的是泛型,而 Output = i32 指定的則是別名,於是這邊我們就能傳進去任何與 i32 相加後會回傳 i32 的東西了,你可以傳入數字,也可以試著把上面的 Num 加上 Add 的定義後傳進去試試, Rust 的編譯器在碰到泛型時會各別的幫出現的每個型態產生程式碼,所以是沒有任何額外的執行消耗的,這也是 Rust 所推的 zero-cost abstract。

泛型還有其它不同的寫法,比如你的型態太長了,那你可以先宣告,再補上 trait 的限制:

#![allow(unused)]
fn main() {
fn print_add_one<T>(n: T)
  where T: Add<i32, Output = i32> {
  println!("{}", n + 1);
}
}

也可以直接寫在參數的宣告那邊,我比較喜歡這樣寫,這是在 Rust 1.27 後新增的語法:

#![allow(unused)]
fn main() {
fn print_add_one(n: impl Add<i32, Output = i32>) {
  println!("{}", n + 1);
}
}

另外你可以回傳實作了某種 trait 的回傳值,同樣是 1.27 的語法:

#![allow(unused)]
fn main() {
fn return_addable() -> impl Add<i32, Output = i32>) {
  42
}
}

收到這個回傳值的使用者只會知道這個型態支援什麼東西,不會知道實際的型態

struct 或 trait 也可以使用泛型:

struct Wrapper<T>{
  inner: T
}

impl<T> Deref for Wrapper<T> {
  type Target = T;
  fn deref(&self) -> &T {
    &self.inner
  }
}

fn main() {
  let n = Wrapper { inner: 42 };
  println!("{}", *n);
}

泛型 & 型態別名

之前一直沒介紹 type alias 的語法,這語法其實並不是隻有在 trait 裡可以使用的,若你覺得某個型態你很常用到但太長了打起來很麻煩時你可以用這個語法來建立一個別名,也可以加上 pub 讓你的別名可以被外部使用:

#![allow(unused)]
fn main() {
pub type MyInt = i32;
}

像標準函式庫中的 std::io::Result 就是一個很好的例子,它的定義如下:

#![allow(unused)]
fn main() {
type Result<T> = Result<T, Error>;
}

這邊定義了有一個泛型的參數的 Result 做為原本的 Result 的別名,之後錯誤的型態則是使用 Error ,於是程式碼裡就不需要到處都是 Result<T, Error> 而只要寫 Result<T> 就可以了。

至於在 trait 中何時該用泛型,何時又該用型態別名呢?

大部份的情況下你應該使用型態別名,不過如果你的 trait 要可以針對不同的型態有不同的處理方式:

#![allow(unused)]
fn main() {
struct Handler;

trait Handle<T> {
  fn handle(input: T);
}

impl Handle<i32> for Handler {
  fn handle(input: i32) {
    println!("This is i32: {}", input);
  }
}

impl Handle<f64> for Handler {
  fn handle(input: f64) {
    println!("This is f64: {}", input);
  }
}
}

這時你該使用的是泛型。

列舉、解構、模式比對

列舉 (Enum)

列舉是 Rust 中的一個型態,其為多個 variant 所組成:

#![allow(unused)]
fn main() {
enum Color {
  Red,
  Green,
  Blue,
}
}

使用時必須要加上列舉的名稱,比如 Color::Red ,或是你也可以像引入函式庫一樣的,把 enum 內的 variant 用 use 引入,比如 use Color::* ,就會把 Color 下的 variant 都引入了。

不過 Rust 的列舉的特殊之處是 variant 可以帶值:

#![allow(unused)]
fn main() {
enum StringOrInt {
  String(String),
  Int(i32),
}
}

裡面的值就像 struct 一樣,也就是你可以不只一個值,或是給它們欄位的名稱:

#![allow(unused)]
fn main() {
enum Point {
  Point2D (i32, i32),
  Point3D {
    x: i32,
    y: i32,
    z: i32,
  }
}
}

在 Rust 中列舉除了用來表示有限的選項外,也可以用來傳遞型態不同的參數,像 StringOrInt 一樣。

另一個重要的應用是 OptionResult ,不知道大家還記不記得之前有提到過 Result 這個代表結果的型態呢,它就是列舉,其定義如下:

#![allow(unused)]
fn main() {
enum Result<T, E> {
  Ok(T),
  Err(E),
}
}

T, E 是泛型的型態變數,分別代表正確時的回傳值,與發生錯誤時的錯誤物件,它是個泛型的列舉,而它也有提供一些方便的方法,比如 expect ,是的 Rust 的列舉是可以幫它定義方法的,同樣的用 impl 就可以了,也可以幫它實作 trait ,它就像 struct 一樣。

Result::expect 其實在之前就有使用過了,它的功能是在結果為 Err 時印出訊息並結束程式。

再來我們剛剛還提到了 OptionOption 是 Rust 中用來代表可能沒有值,它用來取代掉其它語言中的空指標 (null, nil ...) ,它用兩個值 SomeNone 來代表有沒有值, Rust 中 ResultOption 因為很常使用,所以它們跟它們的 variant 都已經被預先引入了,你可以不需要預先引入就可以使用。

它的定義則是這樣的:

#![allow(unused)]
fn main() {
enum Option<T> {
  Some(T),
  None,
}
}

在 Rust 中之所以沒有 null 的,因為 Rust 認為 null 很容易造成錯誤,並且使用 Option 可以強迫使用者先檢查是否有值。

同時 Option 也同樣提供了很多方法可以使用,也有不少跟 Result 有共通的名稱與作用:

  • Option::unwrap: 直接把 Option 內的值取出來,若 OptionNone 則會造成程式印出錯誤訊息後直接結束
  • Option::unwrap_or: 取出值,若沒有值則回傳使用者提供的預設值
  • Option::unwrap_or_else: 取出值,若沒有值則呼叫與使用者提供的函式,並使用回傳值當預設值

unwrap_or_else 常用在建立會需要消耗資源的情況,比如當我們在沒有值時需要空的 String 當預設值就會寫:

#![allow(unused)]
fn main() {
something.unwrap_or_else(String::new);
}

建議可以看一下 ResultOption 的文件,畢竟這兩個型態可說是一定會在 Rust 中接觸到,說不定它已經寫好函式提供你所要的功能了,上面列出的三個函式也都有 Result 的版本。

而 Rust 也幫所有的型態實作了 impl From<T> for Option<T> 可以直接把任何型態的值轉換成 Some

之前雖說沒有預設參數,不過搭配的泛型使用還是可以寫出像這樣的程式碼:

// 這邊的 i 用的是任何可以被轉換成 Option<i32> 的型態
fn print_number(i: impl Into<Option<i32>>) {
  println!("{}", i.into().unwrap_or(42));
}

fn main() {
  print_number(123);
  // 沒有值時還是需要明確傳 None 進去,這邊會印出 42
  print_number(None);
}

解構賦值 (Destructuring)

Rust 中的複合的型態 (陣列、元組、結構) 都可以做解構:

#![allow(unused)]
fn main() {
struct Point {
  x: i32,
  y: i32,
}

let [a, b] = [1, 2];
let (num, msg) = (123, "foo");
let Point { x, y } = Point { x: 10, y: 10 };
}

需要注意的是,解構是轉移所有權的操作,也就是說如果使用到了無法複製的型態,則它的值就會被移動,所以這邊要再來介紹一個關鍵字 ref

#![allow(unused)]
fn main() {
let msg = "Hello world".to_string();

// 底下這兩句的意思是一樣的
let borrowed_msg = &msg;
let ref borrowed_msg = msg;
}

簡單來說 ref 代表要使用 borrow 來取得在右邊的變數,這在解構時非常有用,你可以這樣寫:

#![allow(unused)]
fn main() {
struct Person {
  name: String,
}

let person = Person { name: "John".to_string() };
let Person { ref name } = person;
}

這時候 name 就會用 borrow 的方式取得,而不會把原本的 name 值移出來。

同樣的也有 ref mut

#![allow(unused)]
fn main() {
let Persion { ref mut name } = person;
}

這邊的 name 就會以可寫的方式 borrow,當然這邊也同樣的要套上之前介紹的 borrow 的規則,在 name 的 borrow 結束前你沒辦法使用 person

如果想要在解構時忽略掉某一部份的的值的話怎麼辦呢,如果只想忽略掉某幾個值的話,你可以使用 __ 是個特殊的變數名稱,Rust 不會把任何東西賦值給 _ ,而會直接忽略,你可以想像它就是個黑洞:

#![allow(unused)]
fn main() {
let [_, b, _] = [1, 2, 3];
let Point { x, y: _ } = Point { x: 1, y: 2 }; // 如果 y 的值是不能 copy 的,這邊並不會發生所有權轉移
}

如果想把其它的值都忽略掉呢,你可以使用 .. ,這個目前只支援 struct 與 tuple:

#![allow(unused)]
fn main() {
let (a, ..) = (1, 2, 3);
let Point { x, .. } = Point { x: 1, y: 2 };
}

要注意的是 .. 只能在解構時出現一次

#![allow(unused)]
fn main() {
let (.., x, ..) = (1, 2, 3, 4); // 這裡的 x 應該要是多少呢
}

.. 也可以在你想要忽略掉 struct 中的私有成員時:

#![allow(unused)]
fn main() {
pub struct Person {
  pub name: String,
  age: i32,
}

// 假設這邊是另一個模組,也就是無法取得私有成員的
// 因為你並不知道私有成員的名稱,所以是無法用 _ 的
let { ref name, .. } = person;
}

模式比對 (match)

模式比對是 FP (Functional Programming) 裡一個重要的操作,它的語法如下:

#![allow(unused)]
fn main() {
// 若數字是 1 就印出「數字是 1」,以此類推
match 2 {
  1 => println!("數字是 1"),
  2 => println!("數字是 2"),
  3 => println!("數字是 3"),
}
}

match ,這樣寫的話很像 C 的 switch ,除了它可以有回傳值以及沒有 break 的這點外

那麼它強在哪邊呢,你可以把它搭配上面的解構使用,同時再加上可以帶值的列舉,就能寫出更複雜的判斷,像在 Rust 很常出現的一種錯誤處理方式:

#![allow(unused)]
fn main() {
match result {
  Ok(val) => {
    // 這邊就有成功的值可以用
  }
  Err(err) => {
    // 這邊可以做錯誤處理
  }
}
}

如果你 match 裡放的是大括號的區塊,那可以不用加逗號,怕搞錯的話還是都加吧。

你也可以比對一部份的值比如:

#![allow(unused)]
fn main() {
match [1, 2] {
  [1, _] => println!("陣列的開頭是 1"),
  _ => println!("陣列的開頭不是 1"),
}
}

或是:

#![allow(unused)]
fn main() {
match Point { x: 10, y: 20 } {
  Point { x: 10, .. } => println!("x 是 10"),
  _ => println!("x 不是 10"),
}
}

還可以比對數字是不是在一個範圍內

#![allow(unused)]
fn main() {
match 3 {
  1...5 => println!("x 在 1~5"),
  6...10 => println!("x 在 6~10"),
  _ => println!("x 不在 1~10")
}
}

那個 ... 只有在 match 時支援而已,是代表包含上下界的範圍,如果在平常需要用到包含上下界的範圍,比如在做切片時要用 ..=

#![allow(unused)]
fn main() {
let array = [1, 2, 3, 4, 5];
let slice = &array[0..=2];
}

還可以加上條件判斷:

#![allow(unused)]
fn main() {
match Some(3) {
  Some(x) if x < 5 => println!("x < 5: {}", x),
  Some(x) => println!("x > 5: {}", x),
  None => println!("None"),
}
}

你可以在一行裡比對數個情況

#![allow(unused)]
fn main() {
match 3 {
  1 | 2 | 3 => println!("是 1 , 2 或 3"),
  _ => println!("不是 1 , 2 , 3"),
}
}

你還可以用 @ 來給比對成功的值一個變數

#![allow(unused)]
fn main() {
match 3 {
  // 若這邊比對 1...5 成功,則值會被放到 x 這個變數
  x @ 1...5 => println!("x 是 {}", x),
  6...10 => println!("x 在 6~10"),
  _ => println!("x 不在 1~10")
}
}

Rust 裡的模式比對要求要把所有可能出現的值都比對一次,如果沒有的話會是編譯錯誤, 所以你可以在最後用一個變數,或是不需要變數的話用 _ 當預設的情況,你也可以使用 if ,只在碰到某種情況時處理,而忽略另一些情況:

#![allow(unused)]
fn main() {
// 這邊一定要有 let
if let Some(x) = Some(42) {
  println!("x 是 {}", x);
}
}

下一篇要來講 Rust 的模組架構,這樣就可以好好的組織程式碼,不用在全部都寫在 main.rs 裡了。

Module

在 Rust 中要建立一個模組其實不難,只要像這樣:

mod mymod {
  pub fn print_hello() {
    println!("Hello from mymod");
  }
}

fn main () {
  mymod::print_hello();
}

這樣你就有一個模組了,在裡面的東西只要沒有宣告 pub 的話外面是不能用的:

mod mymod {
  fn private() {
    println!("這是私有的函式");
  }
  pub fn public() {
    println!("這是公開的函式");
    private();
  }
}

fn main () {
  // mymod::private(); // 如果取消註解,這邊會出現編譯錯誤
  mymod::public();
}

但這樣寫還是一樣是寫在同一個檔案裡,如果要分成不同的檔案的話也很簡單,建一個 mymod.rs 跟你的 main.rs 放在一起就好了,資料夾下會像這樣:

src
├── main.rs
└── mymod.rs

之後在你的 main.rs 加上:

#![allow(unused)]
fn main() {
mod mymod;
}

跟上面不同的是這次後面沒有大括號與裡面的內容,這樣 Rust 就會去找看有沒有 mymod.rs 了。

如果想要更複雜一點的話,比如我想要模組下又有子模組呢?這時我們要介紹一個特殊的檔名 mod.rs 就像程式開始執行的地方也就是進入點是 main.rs 一樣,模組的進入點是 mod.rs,我們先在 main.rs 旁建一個叫 mymod 的資料夾,把原本的 mymod.rs 放進去後改名成 mod.rs ,於是結構會變成像這樣:

src
├── main.rs
└── mymod
    └── mod.rs

到這邊你可以試著執行看看,結果應該會跟剛才完全一樣。

我們現在可以在 mymod 的資料夾加上子模組了,這邊加上一個叫 submod.rs 的檔案:

src
├── main.rs
└── mymod
    ├── mod.rs
    └── submod.rs

然後在 mymod/mod.rs 中加上:

#![allow(unused)]
fn main() {
mod submod;
}

到這邊子模組已經可以使用了,如果你想讓外部也能使用你的子模組的話,就在 mod submod 的前面加上 pub 吧:

#![allow(unused)]
fn main() {
pub mod submod;
}

self & super

self 也能被使用在引入的路徑中,它代表的是目前的模組,以上面的 mymod 舉例,如果要在 mod.rs 中使用 submod 中的東西可以寫成:

#![allow(unused)]
fn main() {
use self::submod::something;
// 或把完整的路徑寫出來
use mymod::submod::something;
}

super 則是代表上層的模組,比如:

fn hello() {
  println!("Hello");
}

fn main() {
  sub::call_hello();
}

mod sub {
  use super::hello;
  pub fn call_hello() {
    hello();
  }
}

這邊也可以注意到,子模組可以直接使用上層模組的東西,不論它有沒有宣告為公開的。

use

use 用在引入模組、函式、常數、或列舉中的 variant 等等,其實我們已經使用過很多次了,不過如果要從一個模組中引入很多東西時,你可以不用一行一行的寫,可以像這樣:

#![allow(unused)]
fn main() {
use std::{
  fmt, // 引入子模組
  fs::{File}, // 引入子模組下的 struct
  ops::{Add, Deref}, // 同時引入多個
  io::{self, Read}, // 這行會引入 io 這個模組本身,與在它之下的 Read
};
}

上面的用法你也可以繼續的往下組合,至於可讀性就見仁見智了。

再匯出 (Re-export)

你可以在 use 前面加個 pub 把你引入的模組也再從你這個模組匯出:

mod mymod {
  mod submod {
    pub fn foo() {}
  }

  pub use self::submod::foo;
  
  // 這邊可以使用 foo
}

// 可以從 mymod 引入 foo;
use mymod::foo;
// use mymod::submod::foo; // 這邊拿掉註解會編譯錯誤, submod 是私有的

fn main() {
  foo();
}

有些 crate 所提供的 prelude 就是像這樣,把常用的東西全部在從 prelude 下重新匯出:

#![allow(unused)]
fn main() {
mod mymod {
  pub mod submod1 {
    pub fn often_use1() {}
    pub fn often_use2() {}
    pub fn rare_use() {}
  }

  pub mod submod2 {
    pub fn often_use3() {}
  }

  pub mod prelude {
    pub use super::submod1::{often_use1, often_use2};
    pub use super::submod2::often_use3;
  }
}

use mymod::prelude::*;
}

錯誤處理

現在大多的程式語言都有例外 (exception) ,這讓程式碰到錯誤時可以立即的拋出例外,拋出的例外會中斷目前整個程式的流程,並開始往上找例外處理的程式,可是 Rust 並沒有這種機制。

Rust 中主要是以回傳值 Result 來代表有無錯誤的,此外也有可以立即中止程式的 panic!

panic!

先介紹之前沒用過的 panic!panic! 會直接終止目前的執行緒,如果你呼叫了 Result::unwrapOption::unwrap ,它們也會分別在值為 ErrNone 時發生,這用在程式碰到了無法回復的錯誤。

panic! 也像 println! 是 macro ,所以那個驚嘆號是要加的,此外它裡面也可以放格式化字串,使用方法是一樣的。

主執行緒遇到 panic! 時,程式會印出 panic! 內的訊息,與發生位置後結束程式,你也用以下方法執行程式,你會得到更詳細的輸出:

$ RUST_BACKTRACE=1 cargo run

如果是子執行緒發生 panic 的話等到之後講到多執行緒時再來介紹。

執行緒是電腦執行的單位,如果你的 CPU 有 4 核心,那你的電腦就能一次跑 4 個執行緒,所以現在很多程式為了加速會在一個程式裡產生多個執行緒,同時執行多個工作來加速,現在我們寫的程式都只有一個執行緒,也就是主執行緒而已。

Result

之前有介紹過 Result 是一個列舉,其由兩個 variant 組成,分別是 OkErr ,之前我們有在猜數字的遊戲中使用過 str::parse 來把字串轉換成數字,它的回傳值的型態就是 Result ,但要怎麼知道是 Result 呢?除了看文件也有個簡單的辦法是像這樣:

fn main() {
  let res: i32 = "123".parse::<i32>();
}

拿去編譯的話你就會看到像這樣的錯誤訊息:

error[E0308]: mismatched types
 --> src/main.rs:2:18
  |
2 |   let res: i32 = "123".parse::<i32>();
  |                  ^^^^^^^^^^^^^^^^^^^^ expected i32, found enum `std::result::Result`
  |
  = note: expected type `i32`
             found type `std::result::Result<i32, std::num::ParseIntError>`

或是如果你有裝好 VSCode 的 Rust 的外掛的話應該也可以在滑鼠移上去後看到像這樣的提示:

editor-hint

不過這邊的錯誤也只有可能是字串中有非數字的字元而已,另一個比較複雜的範例是開啟檔案:

#![allow(unused)]
fn main() {
let f = match File::open("myfile") {
  Ok(f) => f,
  Err(err) => {
    // ...
  }
};
}

關於 File 的文件在 std::fs::File

這邊的 errstd::io::Error ,這是在有讀寫,或是比較跟系統底層有關時, Rust 的標準函式庫常回傳的錯誤型態,同時它還有個與之搭配的列舉 std::io::ErrorKind,用來代表錯誤的類別,於是我們可以像這樣使用:

#![allow(unused)]
fn main() {
use std::io::ErrorKind;

let f = match File::open("myfile") {
  Ok(f) => f,
  // kind 是 std::io::Error 才有的方法,將會傳回代表錯誤類型的 ErrorKind
  Err(err) => match err.kind() {
    ErrorKind::NotFound => panic!("找不到檔案"),
    ErrorKind::PermissionDenied => panic!("權限不足"),
    err => panic!("開檔錯誤 {:?}", err),
  }
};
}

像這樣子進行更複雜的處理,也可以在找不到時建立一個檔案也是行的:

#![allow(unused)]
fn main() {
use std::io::ErrorKind;

let f = match File::open("myfile") {
  Ok(f) => f,
  // kind 是 std::io::Error 才有的方法,將會傳回代表錯誤類型的 ErrorKind
  Err(err) => match err.kind() {
    ErrorKind::NotFound => {
      match File::create("myfile") {
        // 檔案建立成功
        Ok(f) => f,
        Err(err) => panic!("無法建立檔案 {:?}", err),
      }
    }
    err => panic!("開檔錯誤 {:?}", err),
  }
};
}

? 運算子

不要懷疑,這個運算子就是 ? ,如果有個函式在它呼叫其它函式時發生了錯誤的情況,它,它就把錯誤往上回傳:

#![allow(unused)]
fn main() {
use std::io::{self, Read};

fn read_and_append<R: Read>(reader: R) -> io::Result<String> {
  let mut buf = String::new();
  match reader.read_to_string(&mut buf) {
    // 成功的話什麼都不用做
    Ok(_) => {}
    // 失敗的話直接回傳錯誤
    err => return err,
  }
  // 假設這邊還要做些處理後才回傳
  buf.push_str("END");
  // 回傳成功的結果
  Ok(buf)
}
}

Read 是所有可讀取的東西會實作的一個 trait ,這包含檔案,或是標準輸入等等關於它的文件在 std::io::Read

其中的判斷錯誤,如果是錯誤就回傳的這段因為太常用到了,所以 Rust 就提供了個簡寫的方法,我們可以直接把上面的 match 那段改寫成:

#![allow(unused)]
fn main() {
reader.read_to_string(&mut buf)?;
}

如果它在成功時是會有回傳值的,比如 File::open 成功會回傳 File ,一個代表檔案的 struct ,那你也可以使用 ?

#![allow(unused)]
fn main() {
let f = File::open("filename")?;
}

? 只能在會回傳 Result 的函式中使用,不過因為它實在是太方便了,所以後來 Rust 的 main 函式也支援回傳 Result 了:

use std::fs::File;
use std::io;

fn main() -> Result<(), io::Error> {
  let f = File::open("filename")?;
  // 因為回傳值變 Result 了,所以這邊要回傳 Ok
  Ok(())
}

該用 panic! 還是回傳 Result

一般的規則就是,能被處理的就用 Result ,嚴重的錯誤才用 panic!

自訂 Error

std::error::Error 中定義了一個代表 Error 的 struct 應該要支援的兩個方法 descriptioncause ,此外同時還要實作 DebugDisplay ,不過實際上 descriptioncause 都有提供預設的實作,於是這些之中一定要實作的就只有 Display 了,此外也可以實作 cause 用來指明發生這個錯誤的原因:

use std::io;
use std::fmt;
use std::error::Error;

// 建一個能包裝 io::Error 的 struct
#[derive(Debug)] // 實作 Debug
struct MyError(Option<io::Error>);

impl fmt::Display for MyError {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    // 寫出自訂的錯誤訊息
    f.write_str("這是自訂的錯誤: ")?;
    match self.0 {
      Some(ref err) => {
        // 如果有包裝的 io::Error 就把它的訊息印出來
        write!(f, "{}", err)
      }
      None => {
        write!(f, "沒有包裝的 io::Error")
      }
    }
  }
}

impl Error for MyError {
  // 覆寫原本的 cause ,在如果有原本的 io::Error 時傳回去
  fn cause(&self) -> Option<&Error> {
    // 這邊很可惜沒辦法用 Option::as_ref
    match self.0 {
      Some(ref err) => Some(err),
      None => None,
    }
  }
}

// 從 io::Error 轉換成 MyError
impl From<io::Error> for MyError {
  fn from(err: io::Error) -> Self {
    MyError(Some(err))
  }
}

fn main() {
  let err = MyError(None);
  println!("{}", err);
  let err = MyError(Some(io::Error::new(io::ErrorKind::Other, "Demo")));
  println!("{}", err);
}

write! 的用法也和 println! 很像,只是第一個參數必須是可以寫入的,也就是有實作 std::io::Write 的物件,所以也可以用在 File 上,而第二個開始才是原本的格式化字串,它不像 println! 一樣會自動加換行。

這邊做了一個我們自己的 Error ,並還包裝了原本的 io::Error ,最後一個 From 的實作其實並不是必要的,只是實作了會很有用:

#![allow(unused)]
fn main() {
fn foo() -> Result<(), MyError> {
  Err(io::Error::new(io::ErrorKind::Other, "Demo"))?;
  unreachable!("這邊永遠不會執行到");
}
}

unreachable! 同樣也是個 macro 它的功能在提示編譯器這種情況不該發生,否則編譯器會認為你的程式可能沒有回傳值,那如果真的執行到了呢?答案是會 panic

這邊可以看到我們用 ? 運算子在碰到 Err(io::Error) 時提早回傳了,只是我們的回傳值明明是寫 MyError 呀,事實上用 ? 運算子回傳時會使用 MyError::from 去轉換回傳的錯誤,當我們有幫 MyError 定義 From<io::Error> 時就能被自動轉換。

當你使用多個第三方的套件時,可能大家都會定義自己的錯誤型態,這時你可以嘗試使用列舉來包裝不同的錯誤型態,同時定義 From 來做轉換,這樣你就能在程式裡使用一個統一的錯誤型態了,因為這件事情太常用了,所以有個叫 failure 的套件就把這件事用比較簡單的方式完成了,可惜因為再介紹下去篇幅會有點長,所以到後面實作專案時再來介紹吧。

題外話,實際上 Result 中代表錯誤的型態並沒有必要實作 Error ,只是一般還是會用實作了 Error 的型態來代表錯誤。

自訂 panic 的訊息 (進階)

你可以在程式開始時註冊一個處理 panic 的函式:

use std::panic;

fn handle_panic<'a>(_info: &panic::PanicInfo<'a>) {
  println!("天啊,程式爆炸了");
}

fn main() {
  panic::set_hook(Box::new(handle_panic));
  panic!("Boom");
}

在這個函式裡你還可以拿到 panic 發生時的位置,與傳給 panic 的訊息:

use std::panic;

fn handle_panic<'a>(info: &panic::PanicInfo<'a>) {
  if let Some(loc) = info.location() {
    println!("在 {} 的第 {} 行", loc.file(), loc.line())
  } else {
    println!("不知道在哪邊");
  }

  // 這邊的 payload 的回傳值是 Any
  // downcast_ref 是嘗試把 Any 這個型態轉換成指定的型態
  // 如果轉換不成功就會回傳 None
  if let Some(msg) = info.payload().downcast_ref::<&str>() {
    println!("訊息: {}", msg);
  } else {
     println!("沒有訊息或訊息不是 str");
  }
  println!("總之爆炸了");
}

fn main() {
  panic::set_hook(Box::new(handle_panic));
  panic!("Boom");
}

Any 是個特殊的 trait ,它幫大部份型態都實作過了一遍,透過編譯器的協助,幫每個型態都分配了一個代碼,要使用時你要使用 downcast_ref 或 downcast_mut ,只要你要求轉換的型態與原本的型態符合就會轉換成功,詳細可以參考文件的 std::any::Any

也有人使用 set_hook 的功能實作了一個會在 panic 時寫出紀錄檔的功能,有興趣可以看看這個專案 human-panic

最開頭的地方說 Rust 沒有例外處理的機制也不是完全正確的, Rust 現在有能力捕捉 panic 了,只是這並沒有保證一定能捕捉到 panic 還要看編譯時的設定…等等的條件,有興趣可以看看 std::panic::catch_unwind ,這功能主要的目的是當你使用其它語言呼叫 Rust 的程式時,讓你可以避免 Rust 的 panic 影響到其它的程式語言,平常如果要回傳錯誤的話還是請用 Result ,不要依賴這個。

單元測試

寫程式難免會出錯,所以我們要寫點程式來測試我們的程式,這邊的程式並不難,你可以試著打開 Rust Playground 一起操作。

#![allow(unused)]
fn main() {
fn add(a: i32, b: i32) -> i32 {
  a + b
}

// 只在測試模式時編譯這個模組
#[cfg(test)]
mod tests {
  // 將上層的東西全部引入
  use super::*;

  // 標記這個是一個測試
  #[test]
  fn test_add_should_work_correctly() {
    // assert_eq! 會確定兩邊是相等的,若不是就會 panic
    assert_eq!(add(1, 1), 2);
    assert_eq!(add(2, 3), 5);
  }
}
}

上面的程式碼當你在 Rust Playground 中輸入後,你應該會注意到上面原本通常都是 RUN 的按鈕變成了 TEST ,按下去就對了,你應該會看到以下結果:

running 1 test
test tests::test_add_should_work_correctly ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

這代表我們的測試通過了,於是我們來加個不會通過的測試看看,請加上面的程式碼中的 tests 模組中:

#![allow(unused)]
fn main() {
#[test]
fn test_this_should_fail() {
  assert_eq!(add(2, 2), 5);
}
}

這次應該會是這樣的輸出:

running 2 tests
test tests::test_add_should_work_correctly ... ok
test tests::test_this_should_fail ... FAILED

failures:

---- tests::test_this_should_fail stdout ----
thread 'tests::test_this_should_fail' panicked at 'assertion failed: `(left == right)`
  left: `4`,
 right: `5`', src/lib.rs:21:7
note: Run with `RUST_BACKTRACE=1` for a backtrace.


failures:
    tests::test_this_should_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

這應該要是個錯誤的範例,然而它真的「錯誤」了,我們來修正它,在 #[test] 的底下加上 #[should_panic] ,像這樣:

#![allow(unused)]
fn main() {
#[test]
#[should_panic] // 標記這個測試應該要失敗
fn test_this_should_fail() {
  assert_eq!(add(2, 2), 5);
}
}

再按一次測試,通過了吧。

你也可以使用回傳 Result 的方式來測試:

#![allow(unused)]
fn main() {
#[test]
fn test_use_result() -> Result<(), &'static str> {
  if add(1, 1) == 2 {
    Ok(())
  } else {
    Err("1 + 1 != 2")
  }
}
}

只是這個方法就沒辦法使用 #[should_panic] 了,看哪個比較方便了,這邊的 ResultErr 中的值只要能用 {:?} 印得出來就行了 (以 Rust 的說法就是有實作 Debug),此外這邊的函式名稱也沒有規定一定要什麼格式,只要有標記 #[test] 就行了,分到另一個模組也是個慣例。

斷言 (Assert)

程式語言常提供的一個功能就是斷言,它可以幫助你測試某些情況是不是真的成立,通常不成立時就會結束程式,上面所使用的 assert_eq! 就是其中一個,它們不只可以被使用在測試中,也可以用在一般程式,確認環境或參數符合假設的必要條件,比如不該用任何數除以 0 。

Rust 中一共提供了三種斷言:

  • assert!: 最基本款的,測試裡面的條件為真,如果裡面的條件為假 (false) 則引發 panic
assert!(false);

thread 'tests::demo' panicked at 'assertion failed: false', src/lib.rs:27:9
  • assert_eq!: 測試兩個參數是相等的,這個上面已經用過了
  • assert_ne!: 測試兩個參數是不相等的
assert_ne!(1, 1);

thread 'tests::demo' panicked at 'assertion failed: `(left != right)`
  left: `1`,
 right: `1`', src/lib.rs:27:9

它們的用法都大同小異,同時它們還支援自訂訊息:

#![allow(unused)]
fn main() {
assert!(false, "是 false");
thread 'tests::demo' panicked at '是 false', src/lib.rs:27:9
}

後面也可以放格式化字串跟參數。

有人可能會擔心這些斷言會不會影響程式的效能,所以上面的斷言也都提供了除錯的版本,分別為: debug_assert!debug_assert_eq!debug_assert_ne! ,它們只在除錯模式時有作用, release 時是沒作用的,除錯模式就是平常使用 cargo build 產生的版本,那 release 版本的怎麼產生呢?使用 cargo build --release 就好了,這會花比較長的時間編譯,此外 cargo run 也可以使用 cargo run --release 執行 release 版本的程式。

函數式程式設計

函數式程式設計的概念是來於數學上的函數,也就是一個輸入對應一個結果,不會受到其它東西的影響,所以程式講究沒有狀態,沒有副作用 (side effect) ,而在 Rust 中也融入了很多函數式程式的概念與設計,比如預設不能修改的變數,以及能直接當成參數傳遞的函式等等。

所謂的副作用是對函式外部的環境或狀態造成改變,所以像 OOP 那樣的修改 struct 甚至是對螢幕輸出都被視為副作用。

Vec

這邊先介紹一個 Rust 中的一個集合型態, Vec 又稱為 vector (中文直翻為向量,但實際用途不同,所以這邊採用原文) , Vec 可以想成一個可以動態成長的陣列,它必須儲存同樣類型的資料,如果你有個不確定大小的連續資料,比起使用陣列來存,用 Vec 來存就會方便很多。

定義一個空的 Vec

#![allow(unused)]
fn main() {
// 因為我們接下來並沒有使用到,所以 Rust 這邊沒辦法自動推導裡面儲存的型態
// 所以只能用 Vec::<i32>::new 這樣的方式指定型態
let v = Vec::<i32>::new();

// 或是指定型態在變數後面
let v: Vec<i32> = Vec::new();
}

直接定義一個有值的 Vec

#![allow(unused)]
fn main() {
let v = vec![1, 2, 3];

// 這會定義一個有 10 個 0 的 Vec
let v = vec![0; 10];
}

vec! 也是 macro , 這邊使用的括號是 [] ,事實上 Rust 中並沒有規定 macro 最外層用來包住參數的括號要使用哪一種,所以 ()[]{} 都是可以用的,只要是成對的就行,這邊使用 [] 只是慣例,其它的單行就使用 () ,多行的則使用 {}

Vec 的尾端增加元素:

#![allow(unused)]
fn main() {
let mut v = Vec::new();

v.push(1);
v.push(2);
v.push(3);

println!("{:?}", v);
}

這個結果會和上面的第一個一樣。

取值:

#![allow(unused)]
fn main() {
let v = vec![1, 2, 3];

// 這邊跟陣列一樣從 0 開始,若取超過範圍會 panic
let n = v[1];

// 這邊回傳的是 Option<&i32> ,若超過範圍只會回傳 None
let n = v.get(1);
}

基本的使用大概是這樣,接下來我們要用到不少 Vec

迭代 (Iterate)

還記得我們的 for 迴圈嗎?如果今天我們把陣列的每個元素都乘 2 並把結果存回一個 Vec 該怎麼寫呢?

#![allow(unused)]
fn main() {
let array = [1, 2, 3];
let mut vec = Vec::new();

// 這邊的 i 型態是 &i32
for i in array.iter() {
  vec.push(i * 2);
}

println!("{:?}", vec);
}

上面的 .iter 會回傳一個迭代器 (iterator) ,讓你可以用 for 迴圈跑過陣列的每個元素,但迭代器所能做到的不只是這樣,以函數式的做法的話會像這樣:

#![allow(unused)]
fn main() {
let array = [1, 2, 3];
// Vec<_> 可以這樣寫是因為中間的型態可以讓編譯器自動推導
let vec = array.iter().map(|x| x * 2).collect::<Vec<_>>();
// 如果你真的不喜歡 ::<Vec<_> 的語法也可以改用型態標註
// let vec: Vec<_> = array.iter().map(|x| x * 2).collect();
println!("{:?}", vec);
}

map 做的事是把每個元素都用其中的函式做轉換,再產生一個新的迭代器。

collect 則是把迭代器的值再蒐集變成某個集合型態,注意的是這邊沒辦法使用陣列,因為它的大小必須在編譯時就決定,而 collect 只能使用能在執行時新增值的型態。

其中的 |x| x * 2 是接下來要介紹的閉包 (Closure) 的語法,它做的事情就是產生一個沒有名字的函式,也稱為匿名函式,將傳進來的參數乘 2 ,等下會詳細介紹語法與怎麼使用。

你也可以寫一個函式來做這件事:

#![allow(unused)]
fn main() {
// 這邊就像一般函式一樣要放型態了
fn time2(x: &i32) -> i32 { x * 2 }

let array = [1, 2, 3];
let vec = array.iter().map(time2).collect::<Vec<_>>();
println!("{:?}", vec);
}

Rust 中在函式裡定義函式並不會出錯喔

如果要計算總合你會怎麼寫呢?請你試著用 for 迴圈寫一個看看吧。

寫好了嗎?我們可以用迭代器的 sum 來做加總:

#![allow(unused)]
fn main() {
let array = [1, 2, 3];
println!("{}", array.iter().sum::<i32>());
}

如果要找出 1 到 10 之間的偶數,使用迭代器該怎麼做呢?

#![allow(unused)]
fn main() {
println!("{:?}", (1..=10).filter(|x| x % 2 == 0).collect::<Vec<_>>());
}

range 本身就是迭代器了,所以直接呼叫方法就行了,這邊使用了 filter 來過濾出符合條件的元素。

Rust 的迭代器是延遲求值的,也就是隻會使用到實際使用到的部份,所以如果使用一個有無限長度的迭代器,但只要只使用到有限的部份就不會出錯,讓我們來做高斯的經典題目吧:

#![allow(unused)]
fn main() {
println!("{}", (1..).take(100).sum::<i32>());
}

1.. 會建一個從 1 開始一直到無限的範圍,但我們之後使用了 take 這使得它只會取前 100 個數字,最後再加總,你應該會看到它印出了 5050 。

如果你把 take 拿掉,它也不會是無窮迴圈就是了,因為電腦整數的大小是有限的, Rust 會避免發生整數溢位,也就是當超過整數上限時,發生了數字變負數的一種情況。

這邊介紹了一些迭代器的方法, Rust 中的迭代器其實挺快的,建議去看一下迭代器的文件,瞭解一下有哪些方法可以用。

迭代器 (Iterator)

所以迭代器到底是什麼,我們實際來操作一次看看:

#![allow(unused)]
fn main() {
let array = [1, 2, 3];
// iter 中必須要記錄目前跑到哪個值,所以必須是 mut
let mut iter = array.iter();

println!("{:?}", iter.next()); // => Some(&1)
println!("{:?}", iter.next()); // => Some(&2)
println!("{:?}", iter.next()); // => Some(&3)
// 已經沒有值了
println!("{:?}", iter.next()); // => None
// 沒有值後再繼續呼叫並不會錯誤,而是一直回傳 None
println!("{:?}", iter.next()); // => None
}

上面的 => 後的結果是印出來的結果,簡單來說迭代器就是每次呼叫 next 就會回傳一個 Option 並包含下一個值。

我們自己來做一個迭代器,讓它從 1 開始產生到指定的數字就停止,如果要做一個迭代器就必須要實作 Iterator 這個 trait :

Iterator 的文件在 std::iter::Iterator ,在這邊還可以看到它提供了哪些方法。

use std::iter::Iterator;

#[derive(Debug, Clone, Copy, Default)]
struct UpToIterator {
  // 這邊都採用無號整數,因為要是有負數很麻煩
  current: u32,
  upper_bound: u32,
}

impl UpToIterator {
  pub fn to(upper_bound: u32) -> Self {
    UpToIterator { upper_bound, ..Default::default() }
  }
}

impl Iterator for UpToIterator {
  // 產生的值的型態
  type Item = u32;

  fn next(&mut self) -> Option<Self::Item> {
    if self.current < self.upper_bound {
      self.current += 1;
      Some(self.current)
    } else {
      None
    }
  }
}

fn main() {
  let mut iter = UpToIterator::to(10);
  for n in UpToIterator::to(10) {
    // 你應該會看到從 1 印到 10 的輸出
    println!("{}", n);
  }
}

重新認識 for 迴圈 (進階)

其實 for 在 Rust 裡只是語法糖:

#![allow(unused)]
fn main() {
let array = [1, 2, 3];
for i in array.iter() {
  println!("{}", i);
}
}

這會被編譯器展開成:

#![allow(unused)]
fn main() {
let array = [1, 2, 3];
{
  // 這是一個編譯器產生的暫時的變數
  let mut _iter = array.iter().into_iter();
  while let Some(i) = _iter.next() {
    println!("{}", i);
  }
}
}

這邊可以看到我們使用了 while let 的語法,這跟 if let 很像,只是變成是如果還是 Some 的話就繼續執行。

into_iter 則是來自於 std::iter::IntoIterator 這個 trait , for 迴圈必須保證它後面的東西是個迭代器,所以會呼叫 into_iter 確保它被轉換成迭代器,相對而言,只要你的型態有實作 IntoIterator 就能被 for 迴圈所使用,要注意的是它會使用掉呼叫它的東西 (也就是它是使用 self 轉移了所有權) ,以下範例:

#![allow(unused)]
fn main() {
let v = vec![1, 2, 3];
// 這邊的 n 的型態是 i32
for n in v {
  println!("{}", n);
}
// 這邊沒辦法再使用 v
}

Rust 中的慣例是若方法的開頭為 into_ 則代表它會消耗掉使用它的東西。

閉包 (Closure)

以上面的例子:

#![allow(unused)]
fn main() {
|x| x * 2
}

宣告傳進來的參數 |x| ,在兩個 | 中放上參數的名字就好了,大多的情況下都不用加上型態宣告,這邊會自動推導,接下來放函式的主體,如果只有一行的話你可以不用加上大括號,或是加上大括號放進多行的程式。

閉包可以存到一個變數去:

#![allow(unused)]
fn main() {
let f = |x| x * 2;
println!("{}", f(10));
}

若你需要宣告型態的話:

#![allow(unused)]
fn main() {
let f = |x: i32| -> i32 { x * 2 };
}

只是這邊就一定要加上大括號了。

閉包可以取得區域變數:

#![allow(unused)]
fn main() {
let n = 3;
let f = |x| x * n;
println!("{}", f(10));
}

預設閉包為用唯讀 borrow 來取得外部的變數,如果加上 mut 宣告,則閉包會用可寫的 borrow 取得外部的變數:

#![allow(unused)]
fn main() {
let mut n = 0;
let mut counter = || {
  n += 1;
  n
};
println!("{}", counter());
println!("{}", counter());
println!("{}", counter());
}

若要讓閉包取得外部的變數的所有權,可以加上 move 關鍵字:

#![allow(unused)]
fn main() {
let v = vec![1, 2, 3];
let is_equal_v = move |a| v == a ;
println!("{}", is_equal_v(vec![1, 2, 3]));
println!("{}", is_equal_v(vec![4, 5, 6]));
// 這邊無法使用 v
}

至於要如何寫一個接受閉包的函式呢?

fn call_closure<F: Fn(i32) -> i32>(work: F) {
  println!("{}", work(10));
}

fn main() {
  call_closure(|x| x * 2);
}

Rust 中有三個代表函式與閉包有關的 trait 分別是:

  • FnOnce: 這個代表它可能會消耗掉它取得的區域變數,所以它可能只能呼叫一次,這對應到上面使用了 move 的閉包
  • FnMut: 這代表它會修改到它的環境,這對應了宣告 mut 的閉包
  • Fn: 這是不會動到環境的閉包,對應到一般的閉包

這讓你可以視你的需求選擇使用哪一個,此外,位在列表上面的 trait 也可以接受位在它以下的 trait ,所以 FnOnce 也接受 FnMutFn ,而 FnMut 接受 Fn ,而三個也都接受一般的函式。

Rust 中也可以讓你傳進一般的函式,所以可以有以下的用法:

#![allow(unused)]
fn main() {
// 為 None 時建一個空字串
println!("{:?}", None.unwrap_or_else(String::new));

// 為 None 時用預設值
// 其實這個有 unwrap_or_default 可以用
println!("{}", Option::<i32>::unwrap_or_else(None, Default::default));

// 全部包進 Some 裡面
// Rust 中可以把 tuple struct 當函式用
println!("{:?}", vec![1, 2, 3].into_iter().map(Some).collect::<Vec<_>>());
}

上面那些大概知道就好了,主要是一些讓你可以少建立一個閉包的寫法。

智慧指標 (Smart Pointer) 與集合型態 (Collection)

集合型態

這邊介紹的集合型態只會再介紹 HashMap 與 HashSet ,不過 Rust 實際上並不只這兩種而已,詳細建議看一下 std::collections ,這邊的東西就是常見的資料結構的實作。

HashMap

HashMap 是由鍵值 (Key-Value) 對應的一個型態,給定一個鍵就能找到一個值,這用在你需要查表之類的時候很好用。

建立 HashMap :

#![allow(unused)]
fn main() {
use std::collections::HashMap;

let mut map = HashMap::new();
map.insert(String::from("key1"), 1);
map.insert(String::from("key2"), 2);
}

也可以從鍵值對建立:

#![allow(unused)]
fn main() {
let map = vec![
  (String::from("key1"), 1),
  (String::from("key2"), 2),
].into_iter().collect::<HashMap<_, _>>();
}

另外還有 maplit 這個 crate 可以使用:

#![allow(unused)]
fn main() {
// 如果要從 crate 引入 macro 要用 #[macro_use]
#[macro_use]
extern crate maplit;

let map = hashmap! {
  String::from("key1") => 1,
  String::from("key2") => 2,
};
}

如果要用鍵取得值:

#![allow(unused)]
fn main() {
// 這邊的鍵要用 borrow 型態的,所以用 str 也是可以的
// 如果沒有這個鍵的話會 panic
println!("{}", map["key1"]);
// 這個做法的話會回傳 Option
println!("{:?}", map.get("key2"));
}

更新值只要再 insert 就行了:

#![allow(unused)]
fn main() {
map.insert(String::from("key1"), 3);
}

或是使用 entry 這個 API :

#![allow(unused)]
fn main() {
map.entry("key1".to_owned()).and_modify(|v| { *v = 3 });
}

HashSet

HashSet 就是數學上的集合,其中不會有重覆的值,很適合用來檢查一個值是否出現過。

建立一個 HashSet :

#![allow(unused)]
fn main() {
use std::collections::HashSet;

let mut set = HashSet::new();
set.insert("foo".to_string());
set.insert("bar".to_string());
}

若用 maplit 的話:

#![allow(unused)]
fn main() {
let set = hashset! { "foo".to_string(), "bar".to_string() };
}

判斷是不是有這個值:

#![allow(unused)]
fn main() {
set.contains("foo");
}

智慧指標

智慧指標是一種會自動在建立時分配一塊記憶體,並在變數消失時自動釋放空間的容器,主要就只有 BoxRc ,文件分別在 std::boxed::Boxstd::rc::Rc

Box

Box 可以用來裝那些無法在編譯時知道大小的型態,同時它也有指標的特性,也能把大小推遲到執行時決定,如果有些情況實在是過不了 borrow checker 的話就用 Box 吧。

建立一個 Box 很簡單:

#![allow(unused)]
fn main() {
let x = Box::new(42);
}

Rust 中其實有個有趣的問題,如果今天要建立一個連結串列 (linked-list) 要怎麼辦呢?

#![allow(unused)]
fn main() {
enum List {
  Cons(i32, List),
  Nil,
}
}

上面這個拿去編譯會得到這個錯誤:

error[E0072]: recursive type `List` has infinite size
 --> src/lib.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^ recursive type has infinite size
2 |   Cons(i32, List),
  |             ---- recursive without indirection
  |
  = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `List` representable

因為編譯器必須在編譯時決定它的大小,可是 Cons 的大小因為會一直遞迴下去,所以無法決定,那我們改用 Box 看看:

#![allow(unused)]
fn main() {
enum List {
  Cons(i32, Box<List>),
  Nil,
}
}

這次就能通過編譯了,也能使用:

#![allow(unused)]
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
}

Box 與 borrow 的差別:

  • Box 本身就擁有資料,所以不會有 lifetime 的問題
  • Box 是分配在 heap ,而 borrow 的資料位置則不一定

電腦主要資料儲存都是在記憶體中,根據其特性還可以再細分,其中有 stack 為存放區域變數,在上面的資料會在函式結束時釋放, heap 則為一塊空間,可讓使用者手動分配記憶體,並可自行決定何時還回去,此處為 Box 所使用的位置,當然分配與歸還都由 Box 自動的管理了。

Rc

Rc 是 reference counting 的意思, Box 的擁有者一次只能有一個人,然而 Rc 可以由多人同時持有,它像 Box 一樣會自動分配記憶體存放資料,並在最後一個人釋放掉 Rc 時將記憶體也釋放。

#![allow(unused)]
fn main() {
use std::rc::Rc;

let a = Rc::new(42);
let b = Rc::clone(&a);
}

以上的 a 與 b 同時持有同一份資料,通常還會搭配類似 Cell 的東西做使用,只是那不是這次的範圍,就先知道一下這個東西吧。

另外還有在 std::sync::ArcArc ,它可以讓多個執行緒同時持有同一份資料。

Weak

若有兩個 Rc 互相持有對方的話,就會因為雙方都屬於有效的持有狀態,而無法正常釋放記憶體,所以就有 WeakWeak 不會增加 Rc 的持有人數。

下一篇要來介紹 CellRefCell ,它們可以讓 Rust 的變數沒有宣告 mut 也能改變,同時讓 borrow check 的規則延遲到執行時才判斷。

Cell 與 RefCell

CellRefCell 能讓變數沒宣告 mut 也能修改,因為有時你還是需要在有多個 borrow 的情況下能修改變數,它們的文件都在 std::cell 底下。

Cell

Cell 使用在能 Copy 的型態,因為它是使用取值與設定值的,在取值時會發生複製,若型態無法複製的話就無法取值。

#![allow(unused)]
fn main() {
use std::cell::Cell;

let num = Cell::new(42);
num.set(123);
assert_eq!(num.get(), 123);
}

RefCell

RefCell 則能使用 borrow 的方式取值,就可以使用在其它的型態上了,它依然會檢查 Rust 的那些 borrow 的規則,只是是在執行時檢查。

#![allow(unused)]
fn main() {
use std::cell::RefCell;

let s = RefCell::new(String::from("Hello, "));

// 以可寫的方式 borrow
s.borrow_mut().push_str("World");

// 唯讀的 borrow
println!("{}", s.borrow());
}

搭配 Rc

RefCell 搭配 Rc 使用就能做出同時有多個參照,卻還能視情況修改值了,我們試著做一個 double linked-list 吧:

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
  data: i32,
  next: Option<Rc<RefCell<Node>>>,
  prev: Option<Weak<RefCell<Node>>>,
}

type Link = Option<Rc<RefCell<Node>>>;

#[derive(Debug, Default)]
struct DoubleList {
  head: Link,
}

impl DoubleList {
  fn new() -> Self {
    Default::default()
  }

  fn push(&mut self, val: i32) {
    match self.head {
      Some(ref head) => {
        let mut cursor = Rc::clone(head);
        let mut next = None;
        let mut prev = Weak::new();
        loop {
          match cursor.borrow().next {
            Some(ref node) => {
              // 先把下一個節點存起來
              next = Some(Rc::clone(node));
            }
            None => {
              // 存前一點節點
              prev = Rc::clone(&cursor);
              break;
            }
          }
          // 這邊 cursor 的 borrow 才結束,我們才能修改 cursor 的值
          cursor = next.unwrap();
        }
      cursor.borrow_mut().next = Some(Rc::new(RefCell::new(Node {
        data: val,
        next: None,
        prev: Some(prev),
      })));
      },
      None => {
        self.head = Some(Rc::new(RefCell::new(Node {
          data: val,
          next: None,
          prev: None,
        })));
      }
    }
  }
}

fn main() {
  let mut list = DoubleList::new();
  list.push(1);
  list.push(2);
  println!("{:?}", list);
}

程序與執行緒

偶爾我們要呼叫外部的程式來幫我們處理一些東西,這個時候就是 std::process 下的東西登場的時候了。

Command

Command 可以讓我們呼叫外部的程式:

#![allow(unused)]
fn main() {
use std::process::Command;

let mut cmd = Command::new("ls").spawn().expect("Fail to spawn");
cmd.wait().expect("Fail to wait");
}

這會呼叫 ls 列出目前目錄下的檔案,並等待子程序結束。

如果你想要像 C 或其它語言的 system 一樣,你需要呼叫 shell 。

執行緒 (Thread)

現在的電腦大多是多核心的,如果建立執行緒,讓多個執行緒一起處理資料的話理論上就能加快速度了:

use std::thread;
use std::time::Duration;

fn main() {
  // 建立執行緒
  let handle = thread::spawn(move || {
    let half_sec = Duration::from_millis(500);
    for _ in 0..10 {
      println!("Thread");
      // 休息半秒
      thread::sleep(half_sec);
    }
  });
  let one_sec = Duration::from_secs(1);
  for _ in 0..5 {
    println!("Main");
    // 休息 1 秒
    thread::sleep(one_sec);
  }
  // 等待子執行緒結束
  handle.join().unwrap();
}

這個與接下來的範例都請在自己的電腦上執行,這邊你應該會看到它們兩個以不同的速度在輸出。

Arc 與 Mutex

還記得之前介紹 Rc 時提過 Arc 嗎? RcArc 很像,只是 Arc 能跨執行緒的共享資料,在使用多執行緒時我們必須要使用 Arc ,而 Mutex 是什麼? Mutex 中文為互斥鎖,功能是保護資料不會因為多個執行緒修改而發生資料競爭 (data racing) 或是其它競爭情況,這邊並不詳細解釋,有興趣可以看一下維基的說明, Rust 中跟其它語言不同,一個有趣的地方是 Mutex 也是個容器,讓你直接包裝需要保護的資料:

use std::thread;
use std::sync::{Arc, Mutex};
use std::time::Duration;

fn main() {
  // 這邊使用 Arc 來包裝 Mutex 這樣才能在執行緒間共享同一個 Mutex
  let data = Arc::new(Mutex::new(0));
  let mut children = Vec::new();
  let one_sec = Duration::from_secs(1);
  // 開 4 個執行緒,因為我的電腦是 4 核的,你可以自己決定要不要修改
  // 另外你也可以把數字改成 1 ,這樣就跟沒有平行化是一樣的,你可以比較一下速度的差別
  for i in 0..4 {
    // 使用 clone 來共享 Arc
    let data = data.clone();
    children.push(thread::spawn(move || loop {
      {
        // 鎖定資料
        let mut guard = data.lock().unwrap();
        // 如果大於 10 就結束
        if *guard >= 10 {
          println!("Thread[{}] exit", i);
          break;
        } else {
          // 處理資料
          *guard += 1;
        }
        println!("Thread[{}] data: {}", i, *guard);
        // 離開 scope 時會自動解鎖 Mutex
      }
      // 模擬處理的耗時
      thread::sleep(one_sec);
    }));
  }
  // 等所有執行緒結束
  for child in children {
    child.join().unwrap();
  }
}

一般都會盡可能減少鎖定資料的範圍,這樣才能盡量的平行化。

rayon

rayon 是個很方便的平行化的 crate ,它可以讓原本迭代器做的事情變成平行化處理,而且使用非常簡單:

extern crate rayon;

// 引入必要的東西
use rayon::prelude::*;

fn main() {
    let mut data = Vec::new();
    for i in 1..=1000000 {
        data.push(i);
    }
    // 只要在原本的 iter 前加上 par_ 就行了
    data.par_iter_mut().for_each(|x| *x *= 2);
}

更多的執行緒: Atomic 、 Channel 與 Crossbeam

今天要來介紹 Atomic 與 Channel ,另外還會介紹 crossbeam 這個 crate 。

這篇的範例也都請在自己的電腦上測試。

Atomic

還記得我們在上一篇時使用了 mutex 來保護我們的數字的讀寫嗎?今天要介紹的是 atomic ,它保證操作不會因為多執行緒中斷,所以可以安全的讀寫,而不需要 mutex ,它的文件在 std::sync::atomic ,我們把昨天的範例用 atomic 重寫一份看看:

use std::thread;
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
use std::time::Duration;

fn main() {
  let data = Arc::new(AtomicUsize::new(0));
  let mut children = Vec::new();
  let one_sec = Duration::from_secs(1);
  for i in 0..4 {
    let data = data.clone();
    children.push(thread::spawn(move || loop {
      let n = data.fetch_add(1, Ordering::SeqCst);
      // 如果大於 10 就結束
      if n >= 10 {
        println!("Thread[{}] exit", i);
        break;
      }
      println!("Thread[{}] data: {}", i, n);
      // 模擬處理的耗時
      thread::sleep(one_sec);
    }));
  }
  // 等所有執行緒結束
  for child in children {
    child.join().unwrap();
  }
}

這個範例其實和昨天的執行結果不太一樣,首先,我們資料的值是有可能超過 10 的,再來它不會像昨天的一樣照著順序了,因為昨天的輸出也在 mutex 的保護範圍內,但這次有保證的只有數值的增加而已,輸出的順序是沒有任何保證的,關於 atomic 的 ordering 建議可以自己再上網找相關的資料,畢竟這個還挺複雜的,怕用錯的話還是用 mutex 就好了。

Channel

Channel 可以跨執行緒傳遞資料,大多的用途是主執行緒用來分配工作給子執行緒,文件在 std::sync::mpsc , Rust 中內建的 channel 是支援多個發送端,但只能有單個接收端:

use std::{
    io::{stdin, BufRead},
    sync::mpsc::channel,
    thread,
};

fn main() {
  // tx 是發送端, rx 是接收端
  let (tx, rx) = channel();
  let handle = thread::spawn(move || loop {
    match rx.recv() {
      Ok(val) => {
        println!("收到 {:?}", val);
      }
      // 出錯時離開迴圈
      Err(_) => break,
    }
  });
  for line in stdin().lock().lines() {
    let line = line.unwrap();
    // 把輸入送過去
    tx.send(line).unwrap();
  }
  // 關掉發送端,這會讓接收端的 recv 得到 Err
  drop(tx);
  // 等待子執行緒結束
  handle.join().unwrap();
}

如果你有用過 Go 的話你應該知道 Go 內建的 channel , Rust 的跟 Go 的 channel 的也挺像的,只是並不像 Go 的一樣可以有多個接收端,所以用 mutex 保護接收端也是有的。

Channel 在多執行緒上非常的方便,可是 Rust 的標準函式庫所提供的 channel 只能支援單個接收端,也不支援同時處理多個接收端,看哪個的訊息先到 (目前這個功能還沒穩定) ,所以就有人做了 crossbeam 這個 crate ,它提供很多多執行緒下會使用到的東西,可以說是補足了 Rust 標準函式庫不足的部份。

crossbeam

crossbeam 實際上不只是一個 crate ,其底下還分成 crossbeam-epochcrossbeam-dequecrossbeam-channelcrossbeam-utils ,這次主要要介紹的東西在 crossbeam-channelcrossbeam-utils ,不過為了方便,我們還是使用 crossbeam 這個 crate 吧。

以下的程式使用的是 crossbeam 0.5

crossbeam channel

extern crate crossbeam;

use std::{
  io::{stdin, BufRead},
  thread,
};
use crossbeam::channel::unbounded;

fn main() {
  // 建一個沒有大小限制的 channel
  let (tx, rx) = unbounded();
  let mut children = Vec::new();
  // 這次建立了 4 個執行緒來展示 crossbeam 能支援多個接收端
  for i in  0..4 {
    let rx = rx.clone();
    children.push(thread::spawn(move || loop {
      match rx.recv() {
        Ok(val) => {
          println!("Thread[{}]: 收到 {:?}", i, val);
        }
        Err(_) => break,
      }
    }));
  }
  let stdin = stdin();
  for line in stdin.lock().lines() {
    let line = line.unwrap();
    tx.send(line).unwrap();
  }
  drop(tx);
  for handle in children {
    handle.join().unwrap();
  }
}

crossbeam 的 channel 比起標準函式庫裡的要強大的多了。

如果你用過 Go 的話, crossbeam 的 channel 比較像 Go 的 channel 。

Scoped Thread

一般而言 thread 可以在背景執行,只要主執行緒沒有結束,子執行緒也可以繼續執行下去,在 Rust 裡要是弄丟了 JoinHandle (thread::spawn 的傳回值) , 執行緒就會脫離掌握了,除非主執行緒結束不然是不會停止的,也代表 Rust 的執行緒可以離開建立它的函式繼續執行,因此 Rust 中的執行緒若要使用 borrow 就必須要有 'static 的 lifetime ,若要使用函式中的 borrow 就要用到 BoxArc 來確保子執行緒能拿到合法的 borrow ,或著,如果有種執行緒能夠保證在函式結束時一起結束,而能拿到函式中的 borrow 就好了。

extern crate crossbeam;

use std::io::{stdin, BufRead};
use crossbeam::{thread, channel::unbounded};

fn main() {
  let (tx, rx) = unbounded();
  thread::scope(|scope| {
    for i in  0..4 {
      let rx = rx.clone();
      // 改呼叫 scope 上的 spawn
      scope.spawn(move |_| loop {
        match rx.recv() {
          Ok(val) => {
            println!("Thread[{}]: 收到 {:?}", i, val);
          }
          Err(_) => break,
        }
      });
    }
    // 要把外面的讀 stdin 移進來,不然不會被執行到而導致程式卡住
    for line in stdin().lock().lines() {
      let line = line.unwrap();
      tx.send(line).unwrap();
    }
    drop(tx);
    // 所有的 thread 會在離開 scope 時 join
    // 所以 lifetime 只需要在這個範圍有效就行了
  }).unwrap();
}

這兩章的內容需要你對執行緒有點基本的瞭解,希望你還能夠理解,這已經是第 17 篇了, Rust 做為一門系統程式語言,接觸到一些電腦、作業系統的基本概念也是不可免的,雖然這兩章的內容對初學程式來說並不是那麼的必要,到這邊我有點好奇各位為什麼想學習 Rust 這門程式語言,如果你完全沒有電腦的基礎概念就來學這門語言我想應該會很辛苦,如果你單純用過 C/C++ 這類的程式語言的話或許會好一點,不知道各位在讀這兩章之前,知不知道 data racing 是什麼呢?

下一篇我們來講所謂「不安全」的 Rust ,在保證安全的 Rust 中為了安全總是犧牲掉了點彈性,而我們要來使用那些不安全的功能,當然,使用了這些功能 Rust 就沒辦法保證你的程式是安全不會有記憶體錯誤,就跟你拆開保固中的東西一樣,要自己負責。

「不安全」的 Rust

Rust 透過編譯器的檢查來保證記憶體的安全,然而這些檢查並不是完美的,總是有誤判的時候,所以 Rust 也提供了這些被標記為不安全的功能,讓使用者可以直接存取底層的東西,相對的編譯器無法對這個部份提供任何保證。

只要使用 unsafe 這個關鍵字就能使用這些不安全的功能,這些功能有:

  • 直接存取指標
  • 修改全域變數
  • 被標記為不安全的方法與 trait

unsafe 實際上並沒有關掉 borrow checker ,也不代表裡面的程式碼就一定是不安全的,只是若使用到了這些功能的話,使用者要自己負責保證安全性而已。

指標

指標代表的是記憶體位置,直接操作指標意謂著直接對記憶體操作,同時指標並不受到 Rust 的 borrow checker 的規則所限制,使用者可以自由的修改它所指向的值,也可以有 null 所以使用者要自己做檢查:

如果你有寫過 C/C++ 的話,這邊的指標跟它們的是一樣的。

use std::ptr;

fn main() {
  unsafe {
    // 取得一個可變的 null 指標,一般而言 null 是指向 0 的位置
    // 這邊還是要加 mut 不然 p 會是唯讀的,之後會無法修改 p 本身
    let mut p: *mut i32 = ptr::null_mut();
    // :p 可以用來印出指標本身
    println!("p: {:p}", p);
    // 分配一個存數字的空間後轉換成指標,這邊改了 p 本身,所以需要 mut
    // 這像是 C 裡的 malloc
    p = Box::into_raw(Box::new(42));
    println!("p: {:p}", p);
    // 直接修改指標指向的值
    *p = 123;
    // 指標不受 borrow checker 的限制,同時這邊沒有加 mut
    let q = p;
    println!("q: {:p}", q);
    let val = Box::from_raw(p);
    println!("val: {}", val);
    // 這個指標依然指向 val 的位置,所以可以修改,同時這邊沒有 mut 也能修改
    *q = 42;
    println!("val: {}", val);
  }
}

Playground

指標的型態有兩種 *mut*const ,後面則要再加上指向的型態名稱,比如 *mut i32*mut 所指向的內容可以修改, *const 則不行,另外 *mut 可以直接轉型為 *const ,但 *const 則要用 as 明確的轉型,然而它們都不受 borrow checker 的限制,所以你可以有多個 *mut 指向同一個位置也是可以的。

另外也可以直接把 borrow 轉型成 pointer :

#![allow(unused)]
fn main() {
let val = 42;
let p = &val as *const _;
}

Unsafe Method

// 在函式前加上 unsafe 就可以標記這個函式是不安全的
unsafe fn foo () {}

fn main() {
  unsafe {
    // 如果不在 unsafe 裡呼叫的話就會編譯錯誤
    foo();
  }
}

不安全的函式或方法通常代表它們需要額外的檢查才能確保它們的使用是安全的,比如像 Vec::set_len 這個方法可以直接修改 Vec 內容的長度:

#![allow(unused)]
fn main() {
// 如果用 new 的話 Vec 是不會分配空間的
let vec = Vec::<i32>::with_capacity(1);
unsafe {
  vec.set_len(100);
}

// 這邊可以看到 vec 所分配的大小實際上還是隻有 1
println!("{}", vec.capacity());
// 然而因為長度已經被設定成 100 了,所以可以看這邊印出了 100 個元素
println!("{:?}", vec);
}

上面這個範例平常可別模仿,這其實已經存取到不該存取的記憶體了,當然這也就是 unsafe 的威力。

extern

Rust 中可以很方便的呼叫 C 語言,之後會再來詳細的講解這部份的,不過在 Rust 中因為並不確定這些外部的函式的安全性,所以這些函式都會是 unsafe 的。

// 直接像這樣宣告外部的函式就好了
extern "C" {
  // atoi 是 C 的標準函式庫中把字串轉換成數字的函式
  fn atoi(s: *const u8) -> i32;
}

fn main() {
  let num = unsafe {
    // C 的字串必須以 0 結尾,同時這邊做轉型成指標
    atoi("42\0".as_bytes().as_ptr())
  };
  println!("{}", num);
}

Playground

全域變數

因為修改與讀取可修改的全域變數可能會有 data racing 等等的問題,所以 Rust 中將這個視為一個不安全的操作:

// 定義全域的變數或常數時一定要加上型態
// 全域變數使用全大寫的名稱是個慣例
static mut VAL: i32 = 42;

fn main() {
  unsafe {
    VAL = 123;
  }
  println!("{}", unsafe { VAL });
}

順帶介紹個 crate 叫 lazy_static ,之後用到時會再做詳細的介紹的,它的功能是建立一個靜態且延遲初始化的變數,也就是像全域變數一樣,但只會在第一次使用時才做初始化,它的 github 在 https://github.com/rust-lang-nursery/lazy-static.rs 。

下一篇我們要來介紹 Rust 2018 有什麼新東西,然後 Rust 的基礎大致上就介紹到這邊了,再接下就進入應用篇,我會開始介紹一些實用的 crate 與實作幾個專案,如果有什麼想要我做的也歡迎提出來。

Rust 2018

Rust 2018 是在今年底預計發佈的 1.31 版本,同時也會有些語法上的改變,如果你想在自己的電腦上使用你必須安裝 beta 或 nightly 版本的 Rust ,使用 rustup 安裝請輸入底下指令:

$ rustup toolchain install nightly

如果你想直接用 rustc 編譯 Rust 2018 的話你需要像這樣:

$ rustc --edition 2018 main.rs

加上 --edition 2018 ,若是要讓專案使用 Rust 2018 的話則要在 Cargo.toml[package] 中加上 edition = "2018"

若你想要在 Playground 中使用 Rust 2018 的話,在上排的選單中可以選擇版本:

Image

NLL

這是 Rust 新的 borrow checker ,它讓你可以編譯像這樣的程式碼:

#![allow(unused)]
fn main() {
let mut array = [0, 1];
let a = &mut array[0];
*a = 2;
let b = &mut array[1];
*b = 3;
}

Playground

這在現在的版本是無法通過編譯的,你可以在 Playground 中使用 Rust 2015 編譯看看,同時 NLL 也提供更好的錯誤訊息。

Module Path

在 Rust 2018 中 module 的路徑都必須要以 crate 的名稱或 cratesuperself 其中一個開頭,其中 crate 代表著目前的專案,原先可以直接使用在同一層的模組的,所以若原本的模組是像這樣:

#![allow(unused)]
fn main() {
mod a {
  fn foo() {}
}
mod b {
  fn bar() {}
}

// 原本可以用

use a::foo;
use b::bar;

// Rust 2018 要用

use self::a::foo;
use self::b::bar;
}

Raw Identifier

Rust 2018 中可以讓你拿原本的關鍵字來當變數名稱等等的了,只是使用起來有點不太一樣:

fn r#match() {
}

fn main() {
  r#match();
}

要加上 r# 還有是點麻煩。

匿名 lifetime

現在可以使用 '_ 在某些非得加上 lifetime 的地方,讓編譯器自動推導了,不過這個功能目前還很不穩定,很容易就把編譯器弄當了,雖然 Rust 的編譯器當掉也只是出現請你回報的錯誤訊息而已,不過會讓錯誤訊息都不顯示也挺麻煩的。

#![allow(unused)]
fn main() {
struct Foo<'a> {
  a: &'a i32,
}

// 原本要寫
impl<'a> Foo<'a> {
}

// 這邊可以用 '_ 讓編譯器去推導了
impl Foo<'_> {
}
}

dyn

原本若傳遞 trait 的 borrow 是寫成 &Trait ,但在 Rust 2018 中要寫成 &dyn Trait ,這基本上是個讓語法更清楚的改進,原本的寫法也還是可以接受的。

有些什麼樣的新功能也可以去看看 Edition Guide ,這是個介紹 Rust 2018 的新功能的文件,另外有興趣也可以看看 Unstable Book 這邊列出了目前還不穩定的功能,雖然大部份都只有名稱跟連結而已,並沒有介紹。

接下來就要開始進入應用篇了,下一篇我要來介紹一些個人覺得常用或好用的 crate 。

Crates 與工具

這篇主要是來介紹 Rust 的社群所提供的方便的工具與 crate ,為避免重覆,若之後有打算深入介紹的我就不在這邊做介紹了。

工具

Clippy

Clippy 是個 Rust 的 linter (程式碼的風格與錯誤檢查工具) ,可以直接用 rustup 安裝:

$ rustup component add clippy-preview

使用也非常簡單,只要在專案裡用以下指令就會做檢查了:

$ cargo clippy

你也可以在 Playground 的右上角找到這個工具:

Image

它能對像這樣的程式碼提示更好的寫法:

fn main() {
  let arr = [1, 2, 3, 4];

  if arr.iter().find(|x| x == &&3).is_some() {
    println!("Found 3");
  }

  let mut iter = arr.iter();

  loop {
    match iter.next() {
      Some(x) => println!("{}", x),
      None => break,
    }
  }
}

它會產生像這樣的提示:

warning: called `is_some()` after searching an `Iterator` with find. This is more succinctly expressed by calling `any()`.
 --> src/main.rs:4:8
  |
4 |     if arr.iter().find(|x| x == &&3).is_some() {
  |        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: #[warn(clippy::search_is_some)] on by default
  = note: replace `find(|x| x == &&3).is_some()` with `any(|x| x == &&3)`
  = help: for further information visit https://rust-lang-nursery.github.io/rust-clippy/v0.0.212/index.html#search_is_some

warning: this loop could be written as a `while let` loop
  --> src/main.rs:10:5
   |
10 | /     loop {
11 | |         match iter.next() {
12 | |             Some(x) => println!("{}", x),
13 | |             None => break,
14 | |         }
15 | |     }
   | |_____^ help: try: `while let Some(x) = iter.next() { .. }`
   |
   = note: #[warn(clippy::while_let_loop)] on by default
   = help: for further information visit https://rust-lang-nursery.github.io/rust-clippy/v0.0.212/index.html#while_let_loop

它並不單單是檢查程式碼風格而已,若有更好或更快的寫法也會提供給使用者,是個很方便的工具。

evcxr

evcxr 是個 Rust 的 REPL ,可以讓你方便的測試一些簡單的程式碼,它的 Github 在 https://github.com/google/evcxr ,如果要安裝的話用:

$ cargo install evcxr_repl

然後就可以在終端機下輸入 evcxr 來使用了,不過它目前很陽春, if 之類的必須輸入在同一樣,也只能用 Ctrl + D 關掉 (Windows 下是 Ctrl + Z ,不過作者並沒有測試過) 。

Crates

先說一下可以去哪邊找 crate ,首先是 awesome-rust ,這邊收集了不少的 crate 並做好了分類,再來是 ergo ,這個專案原本是想收集一些常用的 crate 後整合到一個 crate 裡,不過有段時間沒維護了,但還是可以去看看他收集了哪些,大多都挺好用的。

log

log 是個由社群提出的標準的 log 介面,它在 https://crates.io/crates/log ,其實它只有 API 而已,本身沒有 log 的功能,如果要使用的話要搭配有實作它的 logger ,最簡單的就是 env_logger ,它會根據環境變數決定要顯示哪些 log ,使用起來很簡單:

#[macro_use]
extern crate log;
extern crate env_logger;

fn main() {
  env_logger::init();

  info!("Hello");
}

如果想要自訂性比較高的話可以參考看看 slog ,它提供了可以抽換的各個部份,也有更多的設定,顯示的 log 也比較好看,但相對的也比較複雜點。

num

https://crates.io/crates/num

num 是多個 crate 所組成的,它補足了 Rust 中對數字所不支援的部份,比如大數、分數與複數等等的運算,另外還有 num-iter 提供了支援設定間隔的數字迭代器, num-integernum-trait 提供了一些運算。

regex

https://crates.io/crates/regex

regex 提供了正規表示法的支援,至於正規表示法本身可以上網找一下教學,用在字串處理非常的好用。

lazy_static

https://crates.io/crates/lazy_static

lazy_static 在之前有提過,它提供的是延遲計算的變數,只要在使用到時才會初始化:

#[macro_use]
extern crate lazy_static;

use std::collections::HashMap;

lazy_static! {
  // 一定要加 ref ,因為為了做到延遲初始化, lazy_static 會用另一個型態包裝
  // 總之記得要加就對了
  static ref MAP: HashMap<&'static str, i32> = {
    // 初始化的程式碼可以不只一行
    let mut map = HashMap::new();
    map.insert("foo", 1);
    map.insert("bar", 2);
    map
  };
}

fn main() {
  lazy_static! {
    // 也可以建一個只能在 main 裡使用的變數
    static ref FOO: i32 = 1;
  }
  assert_eq!(MAP.get("foo").unwrap(), &1);
}

lazycell

https://crates.io/crates/lazycell

提供讓你的型態擁有延遲初始化的欄位的功能:

#![allow(unused)]
fn main() {
extern crate lazycell;

use lazycell::LazyCell;

struct Foo {
  lazy_field: LazyCell<i32>,
}

impl Foo {
  pub fn new() -> Self {
    Self { lazy_field: LazyCell::new() }
  }

  pub fn get_lazy(&self) -> &i32 {
    // 若還沒初始化才會執行閉包中的內容做初始化,之後會回傳 borrow
    self.lazy_field.borrow_with(|| {
      42
    })
  }
}
}

derive_more

https://crates.io/crates/derive_more

提供更多可以用在 derive 的 trait 。

nix 與 libc

https://crates.io/crates/nix https://crates.io/crates/libc

libc 提供直接存取底層 API ,而 nix 則是在上面做一層包裝,讓程式碼更安全,不過它們主要都是提供 Linux 下的功能。

用 Rust 呼叫 C 的程式

若是其它的函式庫等等的, crates.io 上可能已經有人提供與那個函式庫的綁定了,可以直接抓來用, Rust 的 crate 的命名慣例中有個若一個 crate 是以 -sys 結尾,代表它提供的是最基礎的綁定,通常它的 API 都是 unsafe 的,若要使用還要花點工夫,也可以找找有沒有高階一點的綁定的函式庫。

綁定 C 函式庫

此篇教學的程式碼會在 https://github.com/DanSnow/rust-intro/tree/master/c-binding

假設我們用 C 寫了個函式庫:

#include <stdio.h>

void say_hello(const char *message) {
  printf("%s from C", message);
}

這個檔案我們放在 lib/hello.c

然後我們要在 Rust 的程式中呼叫:

use std::{ffi::CString, os::raw::c_char};

extern "C" {
  // 宣告一個外部的函式,傳入一個 C 的 char 指標
  fn say_hello(message: *const c_char);
}

fn main() {
  // 建立一個 C 的字串,也就是由 \0 結尾的字串
  let msg = CString::new("Hello").unwrap();
  // 呼叫,並把字串轉換成指標傳入
  unsafe { say_hello(msg.as_ptr()) };
}

字串的部份應該是連結 C 的函式庫時最麻煩的一塊了,建議可以看一下文件的 std::ffi::CString 與跟它搭配的 CStr ,它們就是 Rust 中的 Stringstr 的 C 的版本,通常就是用來傳給 C 的函式庫用的。

這邊我們先嘗試自己手動建置 C 的函式庫,與連結吧,為了簡化這個過程我把建置的過程全部寫成了一個 Makefile 不過這並不是本教學的重點,可以直接去看程式碼的 Makefile 怎麼寫的,也可以上網找找要怎麼寫。

假設我們已經把 C 的部份建置好了,並放在 lib/libhello.a ,接著我們要讓 cargo 知道我們的程式還要去連結這個函式庫才能編譯,我們需要在根目錄下建一個 build.rs ,然後在裡面寫進這樣的程式碼:

fn main() {
  // 告訴 cargo 我們要連結一個靜態的叫 libhello.a 的函式庫
  println!("cargo:rustc-link-lib=static=hello");
  // 告訴 cargo 要加上這個搜尋函式庫的位置
  println!(
    "cargo:rustc-link-search={}",
    // 這邊為了簡化直接使用 concat! 這個 macro 來做字串的連結
    // 正常應該是要用 std::path::PathBuf 之類的來處理不同系統下的不同分隔符號
    // 因為在 Linux 下的分隔符號是 / 但在 Windows 下是 \
    concat!(
      // 這個會是專案的目錄
      env!("CARGO_MANIFEST_DIR"),
      // 我們的函式庫所在的位置
      "/lib"
    )
  );
}

cargo 在建置時會執行 build.rs 並依照裡面的特殊的輸出來處理,詳細可以去看看 cargo 的文件 ,裡面有詳細寫出這些特殊的輸出的用法,此外 build.rs 也可以提供一些不同的功能,比如使用 vergen ,這個 crate 可以幫助我們把目前的版本號存到環境變數,然後我們就可以在編譯時從環境變數讀進目前的版本號一起編譯進程式裡。

不過這樣我們就要靠自己寫 Makefile 來先建置 C 的函式庫才有辦法編譯,有點麻煩,當然 Rust 的社群也想到了這個問題,於是就做了個叫 cc 的 crate ,它能讓我們在 build.rs 中指定要編譯與連結的 C 函式庫,首先我們要先把它加進我們的的 Cargo.toml ,我們可以用這個指令:

$ cargo add cc --build

這個指令要安裝 cargo-edit 才有喔,沒裝的話請去看本系列第三篇

然後把我們的 build.rs 改成:

extern crate cc;

fn main() {
  cc::Build::new().file("lib/hello.c").compile("hello");
}

就這樣,超簡單的, Makefile 也不需要了, cargo 在建置的過程就會去呼叫 gcc 來幫忙編譯 C 的函式庫,並做連結了。

如果要連結系統中的函式庫可以看看 pkg-config 這個 crate ,這應該是 Linux 系統下大部份都支援的一個方式,或是直接使用上面所提到 build.rs 的使用方式來連結系統的函式庫。

bindgen

https://github.com/rust-lang-nursery/rust-bindgen

bindgen 是個由 Rust 寫成的工具,用途是從 C 或 C++ 的標頭檔自動產生 Rust 的綁定,原本這個工具是 Mozilla 所開發的,不過目前已經轉移給 Rust 的社群了,它有指令介面與用在 build.rs 兩種用法,先介紹指令介面,首先先來安裝:

$ cargo install bindgen

接著我們幫剛剛的 hello.c 定義個標頭檔:

#ifndef HELLO_H_INCLUDE
#define HELLO_H_INCLUDE

void say_hello(const char *message);

#endif

使用指令:

$ bindgen hello.h

應該會看到這樣的輸出:

#![allow(unused)]
fn main() {
/* automatically generated by rust-bindgen */

extern "C" {
  pub fn say_hello(message: *const ::std::os::raw::c_char);
}
}

其實這跟我們自己寫的沒什麼兩樣,不過因為是自動產生的,若要幫一些比較大的第三方函式庫做綁定時能自動產生就很方便,於是上面的程式碼就可以直接使用,然後像上面介紹的方法一樣去連結與建置了。

接下來我們用 build.rs 來產生綁定:

extern crate bindgen;
extern crate cc;

use std::{env, path::PathBuf};

fn main() {
  // 剛剛的程式碼
  cc::Build::new().file("lib/hello.c").compile("hello");

  let bindings = bindgen::Builder::default()
    .header("lib/hello.h")
    .generate()
    .expect("Unable to generate bindings");

  // 這會寫到 Rust 所用的暫存資料夾
  let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
  bindings
    .write_to_file(out_dir.join("bindings.rs"))
    .expect("Couldn't write bindings!");
}

然後我們讓原本的程式碼改成引入自動產生的綁定:

#![allow(unused)]
fn main() {
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}

再建置一次,應該會成功,不過時間會花的比較久,因為 bindgen 有點大。

從 C 呼叫 Rust

這次的程式碼在 https://github.com/DanSnow/rust-intro/tree/master/clib

建立可以給 C 使用的函式庫

Rust 跟 C 真的是個很合的語言,要從 C 呼叫 Rust 的程式也很簡單,這次我們來建立一個之前都沒有使用過的函式庫專案:

$ cargo init --lib clib

然後修改 Cargo.toml 加上一段:

[lib]
crate-type = ["staticlib"]

這代表我們要 cargo 建置出可以用來做靜態連結的函式庫,接著來準備個給 C 使用的函式吧,打開 lib.rs 輸入:

#![allow(unused)]
fn main() {
use std::{ffi::CStr, os::raw::c_char};

// 讓編譯器不要修改函式的名稱
#[no_mangle]
// 為了讓函式能夠被 C 呼叫,這邊要加上 extern "C"
pub extern "C" fn say_hello(message: *const c_char) {
  // 包裝 C 的字串成 Rust 的 CStr ,這樣才方便被 Rust 處理
  let message = unsafe { CStr::from_ptr(message) };
  // to_str 會轉換 CStr 成 str ,但如果字串不是合法的 utf-8 編碼就會回傳 Err
  println!("{} from Rust", message.to_str().unwrap());
}
}

Rust 的編譯器會修改函式名稱,加上模組等資訊來避免出現重覆的名稱,另外 Rust 的調用約定 (calling convention) 也與 C 不同,因此必須加上 extern "C" 讓 Rust 使用 C 的調用約定,這樣我們才能直接在 C 使用這些函式。

接著我們來寫 C 的程式吧:

void say_hello(const char *message);

int main() {
  say_hello("Hello");
}

這次我們一樣為了簡化編譯的過程把這部份寫成了一個 Makefile ,可以自己打開來看看,執行的話應該會看到:

Hello from Rust

傳遞 struct

若要把 Rust 的 struct 給 C 使用的話:

#![allow(unused)]
fn main() {
// 加上 repr(C) 可以讓 Rust 的型態具有跟 C 一樣的記憶體結構
#[repr(C)]
pub struct Point {
  x: i32,
  y: i32,
}

#[no_mangle]
pub extern "C" fn create_point(x: i32, y: i32) -> Point {
  Point { x, y }
}
}

C 這邊若要使用:

typedef struct _Point {
  int x;
  int y;
} Point;

Point create_point(int x, int y);

create_point(10, 20);

分配記憶體

如果要在 Rust 裡分配個記憶體並傳給 C 用的話我們可以使用 Box

#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn alloc_memory() -> *mut i32 {
  // 將 Box 轉換成 C 的指標
  Box::into_raw(Box::new(42))
}

#[no_mangle]
pub extern "C" fn free_memory(x: *mut i32) {
  // 從指標建立回 Box ,這樣才能讓 Rust 知道怎麼回收這塊記憶體
  // 這邊使用 drop 明確的清掉這個 Box
  // 不過 Rust 其實也會在 Box 離開有效範圍時自動清掉,所以也不一定要這樣做
  drop(unsafe { Box::from_raw(x) });
}
}

C 的部份:

// 補充一個 C 語言的小知識,若函式的宣告中沒放東西代表的是傳什麼都可以
// 所以我都會習慣在沒有參數時放 void
int *alloc_memory(void);
void free_memory(int *x);

int *x = alloc_memory();
// 這邊可以使用這個變數
*x = 123;
// 記得把空間交回給 Rust 清除
free_memory(x);

務必讓 Rust 清理記憶體, Rust 預設並不是使用 malloc 與 free ,若用 free 來清理是會出問題的,再者,只有 Rust 知道那個型態有沒有其它需要釋放的資源。

傳 Vec

Rust 的 Vec 真的很方便,可以自動的成長,做為陣列使用就不用擔心空間不夠的問題 (除非你的環境的記憶體很珍貴) ,如果要傳遞給 C 使用的話要怎麼辦呢:

#![allow(unused)]
fn main() {
use std::mem;

#[no_mangle]
// 這邊多使用了一個參數,用來回傳長度
pub extern "C" fn create_vec(size: *mut usize) -> *mut i32 {
  let mut vec = Vec::new();
  // 假設做了些工作來產生這個 Vec
  vec.push(1);
  vec.push(2);
  vec.push(3);

  // 讓 Vec 的容量與實際大小一樣
  vec.shrink_to_fit();
  // 一般來說都會一樣,不過這並沒有保證,詳細可以看一下文件
  // 這邊用 assert 確保這種情況不會出現
  assert!(vec.capacity() == vec.len());
  // 回傳大小
  unsafe { *size = vec.len() };
  // 取得指標
  let p = vec.as_mut_ptr();
  // 這讓 vec 不會被 Rust 清除
  mem::forget(vec);
  p
}

#[no_mangle]
pub extern "C" fn free_vec(vec: *mut i32, size: i32) {
  drop(unsafe { Vec::from_raw_parts(vec, size, size) });
}
}

在 C 中使用:

int *create_vec(size_t *size);
void free_vec(int *vec, size_t size);

size_t size;
int *vec = create_vec(&size);
for (size_t i = 0; i < size; ++i) {
  printf("%d ", vec[i]);
}
// 換行
puts("");
free_vec(vec, size);

自動產生 C 的標頭檔

前面我們都自己宣告 C 的函式,這次讓程式來自動幫我們產生標頭檔吧,首先加上 cbindgen

$ cargo add cbindgen --build

然後同樣的我們需要 build.rs

extern crate cbindgen;

use std::env;

fn main() {
    let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();

    cbindgen::generate(crate_dir)
        .expect("Unable to generate bindings")
        // 寫到 bindings.h 這個檔案
        .write_to_file("bindings.h");
}

接著我們需要一個設定檔:

# 不設定的話預設會是 C++
language = "C"
# 設定 C 的 struct 要不要加 typedef
style = "Both"
# 設定 include guard
include_guard = "INCLUDE_BINDINGS_H"

再跑一次 cargo build 應該就會看到它產生一份這樣的標頭檔了:

#ifndef INCLUDE_BINDINGS_H
#define INCLUDE_BINDINGS_H

#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>

typedef struct Point {
  int32_t x;
  int32_t y;
} Point;

int32_t *alloc_memory(void);

Point create_point(int32_t x, int32_t y);

int32_t *create_vec(uintptr_t *size);

void free_memory(int32_t *x);

void free_vec(int32_t *vec, uintptr_t size);

void say_hello(const char *message);

#endif /* INCLUDE_BINDINGS_H */

這樣產生宣告的部份就自動化了,我們的 C 的部份只要負責呼叫就好了,可喜可賀。

實作 Python 的原生擴充與條件編譯

Python 是個廣泛使用的腳本語言,想必或多或少都有聽過這個名字吧,人工智慧、科學計算、統計、應用程式、爬蟲等等的領域無不存在,非常的實用,如果想學個腳本語言的話非常推薦,雖然我個人是 Ruby 派的。

在這方便的程式語言中若偶爾碰到了需要對底層的東西做操作又找不到相關的函式庫時,就只好自己來寫綁定啦,或是有某部份需要加速等等的, Rust 的社群提供了一個很棒的 Python 綁定: PyO3

本次的程式碼在 https://github.com/DanSnow/rust-intro/tree/master/python-binding

PyO3 需要使用到 nightly 版本的 Rust ,因為它目前使用到了一個還未穩定的功能 specialization ,中文翻譯叫「特化」,它可以讓你為特定的型態提供不同的 trait 的實作,實際上標準函式庫中也有因為效能原因而為 &str 提供特化的 to_string 實作。

Rust 只有 nightly 的編譯器支援開啟這些不穩定的功能,但是為了要編譯標準函式庫, Rust 的編譯器使用了個特殊的環境變數允許在穩定版開啟這些功能。

我們先來設定我們的專案與安裝 PyO3 吧,這次比較特別點,我們必須手動修改 Cargo.toml ,在 [dependencies] 後加上:

pyo3 = { version = "0.4.1", features = ["extension-module"] }

features 是代表我們需要的功能, Rust 支援使用者在程式裡設定一些只在某些功能開啟或關閉的情況下才編譯的程式碼,而我們這邊需要開啟 extension-module 這個功能才有辦法把 Rust 的程式編譯成 Python 的擴充模組。

這有點像在 C 裡使用 define 定義常數加上 ifdef 等等來開關功能。

接著我們再加入一段設定:

[lib]
name = "binding"
crate-type = ["cdylib"]

這邊是設定編譯完的輸出檔名與我們要編譯成動態函式庫,這樣才能讓 Python 載入。

總之先像往常一樣寫個 Hello world :

#![allow(unused)]
fn main() {
// 開啟 specialization 這個功能,這只能在 nightly 版才能使用
#![feature(specialization)]

#[macro_use]
extern crate pyo3;

// 直接引入 prelude
use pyo3::prelude::*;

// 標記這是個給 python 的函式,這是 PyO3 提供的功能
// 這些函式必須回傳 PyResult
#[pyfunction]
fn hello() -> PyResult<()> {
  println!("Hello from Rust");
  Ok(())
}

// 標記這是 python 模組初始化用的函式
#[pymodinit]
fn binding(_py: Python, m: &PyModule) -> PyResult<()> {
  // 將我們的函式匯出給 python 使用
  m.add_function(wrap_function!(hello))?;

  Ok(())
}
}

然後我們編譯我們的函式庫:

$ cargo build

如果你編譯失敗的話你會需要安裝 python3-dev

編譯完後你應該看到我們編譯好的檔案在 target/debug/libbinding.so (如果你是用 Windows 的話會是 binding.dll , Mac 會是 libbinding.dylib) ,我們把它複製出來,然後改名成 binding.so (Mac 也一樣, Windows 要改成 binding.pyd) ,接著你可以在終端機下輸入 python3 (或 python) 來打開 Python 的 repl ,或你也可以選擇使用 ipython

>>> import binding
>>> binding.hello()
Hello from Rust

>>> 是 Python 的提示字元,後面的內容是你要輸入的。

這次我們來計算 π (PI) 吧,也就是圓周率,我們用的是蒙地卡羅的計算方法,總之大概就是正方型與其內切圓的面積為 4:PI ,所以我們隨機的打點,然後計算落在圓中的次數來反算 PI ,用 Python 是這樣的:

from random import random

TIMES = 1000_0000


def calculate_pi():
    hit = 0

    for _i in range(0, TIMES):
        x = random()
        y = random()
        if x * x + y * y <= 1.0:
            hit += 1
    return hit * 4 / TIMES

在上面隨機的產生了 1 千萬次的點,然後最後回傳算出 PI 的近似值,接著我們用 timeit 這個 Python 的 module 來算耗時:

print("Python:", timeit.timeit("calculate_pi()", number=3, globals=globals()))

重覆執行三次算平均,在我的電腦上大概花了 4 秒。

接著我們用 Rust 實現同樣的演算法:

#![allow(unused)]
fn main() {
#[pyfunction]
fn calculate_pi() -> PyResult<f64> {
  let mut hit = 0;
  for _ in 0..TIMES {
    let x = rand::random::<f64>();
    let y = rand::random::<f64>();
    if x * x + y * y <= 1.0 {
      hit += 1;
    }
  }
  Ok(f64::from(hit * 4) / f64::from(TIMES))
}
}

一樣在 Python 用 timeit 計算時間,這次只花了 0.7 秒,快了大約 5 倍啊。

最後若我們搭配之前介紹的 rayon 做平行化:

#![allow(unused)]
fn main() {
#[pyfunction]
fn calculate_pi_parallel(py: Python) -> PyResult<f64> {
  let hit: i32 = py.allow_threads(|| {
    (0..TIMES)
      .into_par_iter()
      .map(|_| {
        let x = rand::random::<f64>();
        let y = rand::random::<f64>();
        if x * x + y * y <= 1.0 {
          1
        } else {
          0
        }
      })
      .sum()
  });
  Ok(f64::from(hit * 4) / f64::from(TIMES))
}
}

使用到執行緒的部份必須包在 py.allow_threads 裡,不然會出問題,這次只花了 0.3 秒,底下是在我電腦上執行的完整結果:

Python: 4.45852969000407
Rust: 0.7022144030052004
Rust parallel: 0.30857402100082254

PyO3 雖然需要使用到 nightly 版的 Rust ,但它真的很方便,實際上 PyO3 的 macro 幫我們產生了很多在 Rust 與 Python 之間轉換資料型態的程式碼,我們才能這麼簡單的寫與 Python 的綁定,如果有興趣的話有另一個 crate 叫 cpython ,它是 PyO3 的前身,但它並沒有提供這些自動產生的程式碼,所以可以看到這中間做了哪些轉換。

條件編譯

因為上面正好提到了,就順便來講一下,條件編譯不只可以做出可以開關的功能,也能在不同系統或架構下提供不同的功能。

#[cfg(unix)]
fn print_os() {
  println!("你在使用類 Unix 的系統");
}

#[cfg(windows)]
fn print_os() {
  println!("你在使用 Windows 系統");
}

fn main() {
  print_os();
}

上面的程式在 Windows 下與 Linux 下會印出不同的東西,因為 Rust 的編譯器會根據不同的作業系統而選擇編譯不同的程式,這也不只可以用在函式上而已,還可以用在模組的引入:

#![allow(unused)]
fn main() {
#[cfg(unix)]
mod unix_imp;
#[cfg(unix)]
use unix_imp as imp;

// Use imp::...
}

這很類似在標準函式庫中提供在不同系統下不同實作的方法。

另外也可以在 Cargo.toml 中宣告 [features]

[features]
default = [] # 預設開啟的功能
foo = [] # 一個叫 foo 的功能,後面可以放相依的功能或是支援這個功能所額外需要的 crate

然後在程式碼中使用:

#![allow(unused)]
fn main() {
#[cfg(feature = "foo")]
fn foo() {
  // 實作 foo 功能等等
}
}

這篇展示了怎麼建立 Python 的綁定,大部份的腳本語言都有支援用 C 擴充它的功能 (但像 Node.js 是用 C++) ,這使得這些腳本語言能透過這些原生的擴充模組存取系統底層或是加速,而只要能夠支援 C 就能透過 Rust 實作,有著接近 C 的速度,卻有如此大量的社群套件支援,用 Rust 來實作這些原生套件真的不失一個選擇。

指令列工具與 HTTP Client

本次的程式碼在 https://github.com/DanSnow/rust-intro/tree/master/hastebin-client

這次來寫個在指令列下使用的小工具吧,各位知道 hastebin 嗎?是個開源,而且很陽春的 pastebin ,今天的目標是來寫個它的用戶端,首先先建立專案並安裝幾個相依套件吧:

$ cargo init hastebin-client
$ cd hastebin-client
$ cargo add atty clap reqwest serde serde_derive

那幾個 crate 功能分別是:

  • atty: 偵測是不是終端機
  • clap: 分析指令列的參數
  • reqwest: Http 的用戶端
  • serde 與 serde_derive : 序列化與反序列化的函式庫,這在 Rust 的生態系中很常用

我們先試著用 reqwest 送出一份文件吧:

#[macro_use]
extern crate serde_derive;
extern crate reqwest;
extern crate serde;

use reqwest::{Client, Url};

const URL: &str = "https://hastebin.com";
// 這邊會把我們的程式碼以字串的型式引入進來
const SOURCE: &str = include_str!("main.rs");

// Deserialize 是 serde 提供的,只要加上去就能從資料反序列化回 Rust 的這個型態
#[derive(Clone, Debug, Deserialize)]
struct Data {
    key: String,
    // 因為這個欄位不一定會有,所以讓 serde 在沒有這個欄位時用預設值
    #[serde(default)]
    message: Option<String>,
}

fn main() -> reqwest::Result<()> {
  // 把網址 parse 成 Url 的型態
  let base = Url::parse(URL).unwrap();
  // 建一個 reqwest 的用戶端
  let client = Client::new();
  let res = client
    // 建立 post 的請求,並在網址後附上 documents
    // 完整的網址就變成 https://hastebin.com/documents 這正是 hastebin 的 api
    .post(base.join("documents").unwrap())
    // body 為我們的原始碼
    .body(SOURCE)
    // 送出請求
    .send()?
    // 將回傳的資料做為 json 反序列化為 data
    .json::<Data>()?;
  // 印出來
  println!("{:?}", res);
  Ok(())
}

實際執行一次會看到這樣的輸出:

Data { key: "avamicupez", message: None }

這邊的 key 是文件的代碼,你的應該不會跟我的一樣,這代表我們的文件在 https://hastebin.com/avamicupez 。

接著我們來加上更多功能吧,我希望這個程式可以讓我接上檔名就可以上傳該文件,若沒有接上檔名時則從標準輸入讀進來,還要顯示上傳後的網址,底下只有大概列一下改變的部份:

// ...
extern crate clap;

use clap::{App, Arg};
use reqwest::{Body, Client, Url};
use std::{
    fs::File,
    io::{stdin, Read},
    process,
};

// ...

fn main() -> reqwest::Result<()> {
  // 設定 clap
  let matches = App::new("haste-client")
    .author("DanSnow")
    .version("0.1.0")
    // 定義一個可選的位置的參數
    .arg(Arg::with_name("FILE").index(1))
    // 取得 parse 的結果
    .get_matches();
  // 決定要怎麼取得 body ,在有給檔名時使用檔名
  let body = if let Some(file) = matches.value_of("FILE") {
    // 開檔
    match File::open(file) {
      // 轉換成 Body
      Ok(f) => Body::from(f),
      Err(err) => {
        eprintln!("開啟檔案失敗: {}", err);
        process::exit(1);
      }
    }
  } else {
    // 這邊是讀標準輸入
    let mut buf = String::new();
    stdin()
      .lock()
      .read_to_string(&mut buf)
      .expect("讀輸入失敗");
    Body::from(buf)
  };
  // ...
  let res = client
    .post(base.join("documents").unwrap())
    .body(body)
    .send()?
    .json::<Data>()?;
  println!("{}", base.join(&res.key).unwrap());
  Ok(())
}

到這邊就完成了一個很陽春的用戶端了,很簡單吧,我們再多加一點功能好了,如果加上參數 --raw 就顯示另一個 raw 版本的網址,如果標準輸入是終端機的話就打開 vim (Linux 中在終端機下的一個老牌的文字編輯器),讓使用者編輯檔案,編輯完再上傳,同樣的,這次只列出修改的部份:

// ...
extern crate atty;

use atty::Stream;
use std::{
  env,
  fs::{self, File},
  io::{stdin, Read},
  process::{self, Command},
};

// ...

fn main() -> reqwest::Result<()> {
  let matches = App::new("haste-client")
    .author("DanSnow")
    .version("0.1.0")
    // 增加 RAW 這個參數,設定參數使用的是 --raw
    .arg(Arg::with_name("RAW").long("raw"))
    .arg(Arg::with_name("FILE").index(1))
    .get_matches();
  let body = if let Some(file) = matches.value_of("FILE") {
    // ...
    // 判斷是不是終端機
  } else if atty::is(Stream::Stdin) {
    // 建個放暫存檔的位置
    let path = env::temp_dir().join("haste-client-tempfile");
    // 啟動 vim 打開這個暫存檔
    let mut child = Command::new("vim")
      .arg(path.as_os_str())
      .spawn()
      .expect("開啟 vim 失敗");
    child.wait().expect("等待 vim 結束失敗");
    // 開啟暫存檔
    let file = File::open(&path).expect("開啟暫存檔失敗");
    // 刪除暫存檔,在 Linux 就算刪除檔案,已經開啟的人還是可以正常讀寫檔案
    // 這招在 Windows 下不適用
    fs::remove_file(path).expect("刪除暫存檔失敗");
    Body::from(file)
  } else {
    // ...
  };
  // 檢查有沒有 RAW 參數,若有則在網址加上 raw
  let mut url = base;
  if matches.is_present("RAW") {
      url = url.join("raw/").unwrap();
  }
  println!("{}", url.join(&res.key).unwrap());
  Ok(())
}

這樣子已經挺好用的了,不過我們再加個參數好了,因為 hastebin 可以自己架設,所以我們加個 --host 的參數讓使用者決定要不要用自己的伺服器,同時也讀取 HASTE_SERVER 這個環境變數來找伺服器,順序是 --host > HASTE_SERVER > 預設值,以下是程式碼:

// ...
fn main() -> reqwest::Result<()> {
  let matches = App::new("haste-client")
    .author("DanSnow")
    .version("0.1.0")
    .arg(
      // 定義一個叫 HOST 的參數,它需要值,這次支援使用 -h 或 --host
      Arg::with_name("HOST")
        .short("h")
        .long("host")
        .takes_value(true),
    )
    .arg(Arg::with_name("RAW").long("raw"))
    .arg(Arg::with_name("FILE").index(1))
    .get_matches();
  // ...
  let base = Url::parse(
    // 先嘗試取得 HOST 的內容並轉換成 String
    &matches
      .value_of("HOST")
      .map(ToOwned::to_owned)
      // 取得環境變數的值,並用 ok 將 Result 轉換成 Option
      .or(env::var("HASTE_SERVER").ok())
      // unwrap ,若沒有值則用預設值
      .unwrap_or_else(|| URL.to_owned()),
  )
  .unwrap();
  // ...
}

另外你也可以對這個程式使用 --help--version 看看,這兩個是 clap 自動幫我們加上去的 clap 還有其它的使用方法,有興趣可以去看文件,還有 serde 系列的還有個叫 serde_json 的函式庫,可以把 Rust 的值轉換成 json 或是從 json 轉換回來,也建議參考看看。

這次實作的 hastebin 的用戶端,我覺得做起來還挺簡單的,你覺得呢? Rust 的社群提供了這些強大的 crate 讓這些功能的實作都變的很容易,雖然同樣的功能用 Python 等等的腳本語言做起來肯定更簡單,但 Rust 的程式碼我並沒有覺得有複雜到哪裡去,而且執行速度又快又是原生的程式,如果你會寫 Go 的話要不要試著寫一個來比較看看呢?

這次的用戶端其實也還有些可以改進的地方,比如:

  • 錯誤處理的部份,像 --host 亂輸入
  • 使用 EDITOR 環境變數來打開使用者偏好的編輯器

或許你也可以想想看有什麼地方能改進的。

Diesel: Rust 的 ORM

在開始之前,我有個想講的東西,不過你也可以跳過這段直接看底下的正文。

昨天的程式碼中的第 68 行所出現的:

#![allow(unused)]
fn main() {
&matches
  .value_of("HOST")
  .map(ToOwned::to_owned)
  .or(env::var("HASTE_SERVER").ok())
  .unwrap_or_else(|| URL.to_owned())
}

老實說,後來想想我不太喜歡這個部份,昨天因為 env::var 回傳的是 String ,而另外兩個是 &str ,為了通過 Rust 的編譯所以必須讓型態統一,而我的做法是把全部都轉成 String ,雖然是在有值的時候才轉換,但傳入網址的地方所要的只是 &str 而已,也就是若建了 String 也只是存活到這個函式呼叫結束而已,如果可以我想讓 &str 保持 &str 就好,但這有可能嗎?有的,這時候就是 std::borrow::Cow 出場了。

Cow 也算是個智慧指標,不知道各位有沒有聽過「寫入時複製 (copy on write)」,簡單來說就是個在發生修改時將內容複製一份來避免修改到原本的內容,同時又可以減少資源的消耗 (若沒發生寫入就沒必要複製),而 Cow 正是這個功能在 Rust 中的實現,你可以把一個唯讀的 borrow 給它,然後在有必要修改時呼叫 to_mutCow 會把 borrow 的值複製一份讓你修改,但若已經複製過了,那就不需要再複製了。

不過這有什麼關係呢?有的, Cow 是個列舉,它有兩個 variant 分別是 BorrowedOwned ,各別代表借來的資料與自己擁有的資料,注意到了嗎?它幫 borrow 的資料與擁有的資料提供了一個統一的介面啊,於是我們把上面那份程式碼中的字串們都用 Cow 包裝起來:

#![allow(unused)]
fn main() {
&matches
  .value_of("HOST")
  .map(Cow::from)
  .or(env::var("HASTE_SERVER").ok().map(Cow::from))
  .unwrap_or(Cow::from(URL))
}

這樣感覺好多了, &str 轉換成 String 必須要把字串的內容複製一份,而 Cow 只會多出一點點的空間耗用就可以同時相容 &strString 了,寫這種系統程式語言就實在是會忍不住去在意這種消耗記憶體的事啊,此外這邊的程式碼已經更新上去了。

Diesel

這次的程式碼在: https://github.com/DanSnow/rust-intro/tree/master/message-board

今天要介紹的是 Diesel ,這是個 Rust 的 ORM 與 Query Builder ,它支援 pgsql 、 mysql 與 sqlite ,並能在編譯時就檢查出部份的語法錯誤 (比如使用到了該資料庫不支援的功能)。

ORM 的中文翻釋是「物件關聯對應」,原本是指將不同系統中的資料對應到程式語言中的物件,不過現在很多都已經變成指這種能連接資料庫,並把查詢結果變成物件的函式庫了。

在開始使用前要先安裝 Diesel 的工具,請輸入以下指令:

$ cargo install diesel_cli

預設它會開啟所有能支援的資料庫,若你只需要它支援部份的資料庫可以用以下指令

$ cargo install diesel_cli --no-default-features --features sqlite

本篇教學只會使用到 sqlite ,若你還想要支援 mysql 可以用逗號隔開 --features sqlite,mysql ,另外也有 postgres ,此外安裝時還會需要對應的系統函式庫,比如若要支援 sqlite 在 Ubuntu 下就要安裝 libsqlite3-dev

設定專案

然後來建立一下專案,這次我們來做一個留言板,不過今天只是先介紹資料庫的使用部份:

$ cargo new message-board
$ cd message-board
$ cargo add dotenv diesel

接著我們需要修改一下 Cargo.toml ,把 diesel 的那行改成如下,開啟 sqlite 的支援:

diesel = { version = "1.3.3", features = ["sqlite"] }

接下來設定資料庫的位置,建一個叫 .env 的檔案後加入一行:

DATABASE_URL=db.sqlite

之後執行:

$ diesel setup

到這邊我們應該會看到 diesel 已經幫我們建好了資料庫的檔案 db.sqlite 與一個資料夾 migrations 還有一個設定檔 diesel.tomlmigrations 這個資料夾是用來放建立與修改資料表的檔案用的。

Migration

我們先建一個存貼文的表格吧:

$ diesel migration generate create_posts

它會在 migrations 的資料夾下建立一個以日期、一組號碼與 create_post 命名的資料夾,在底下會有兩個檔案, up.sqldown.sql 分別為建立的 SQL 與撤消的 SQL ,我們先在 up.sql 中寫入建立資料表的指令:

CREATE TABLE posts (
  id INTEGER NOT NULL PRIMARY KEY,
  author VARCHAR NOT NULL,
  title VARCHAR NOT NULL,
  body TEXT NOT NULL
);

然後在 down.sql 中寫入刪除資料表的指令:

DROP TABLE posts;

接著執行:

$ diesel migration run

它會執行剛剛寫好的 SQL ,同時也會更新 src/schema.rs 這個檔案,你可以打開來,應該會看到以下內容:

#![allow(unused)]
fn main() {
table! {
  posts (id) {
    id -> Integer,
    author -> Text,
    title -> Text,
    body -> Text,
  }
}
}

這個檔案是記錄目前的資料表結構, diesel 指令會幫你維護這個檔案,在編譯時會靠這個檔案來建立關於資料庫的查詢、新增、修改等等的程式碼,我們先在 src/main.rs 裡把它引入吧:

#[macro_use]
extern crate diesel;

mod schema;

fn main() {}

到這邊程式應該可以編譯執行,雖然會有一堆警告,不過那沒關係。

連線資料庫

使用資料庫的第一步當然是跟資料庫做連線,不過其實我們用的是 sqlite ,只是要開個檔案而已:

#[macro_use]
extern crate diesel;
extern crate dotenv;

use diesel::{prelude::*, sqlite::SqliteConnection};
use dotenv::dotenv;
use std::env;

mod schema;

fn establish_connection() -> SqliteConnection {
  let url = env::var("DATABASE_URL").expect("找不到資料庫位置");
  SqliteConnection::establish(&url).expect("連線失敗")
}

fn main() {
  dotenv().ok();
  establish_connection();
}

使用 diesel 建立連線很簡單,只要呼叫對應的連線物件的 establish 並傳入資料庫的位置就行了。

建立 Model

所謂的 ORM 就是把資料庫的資料與這些被稱為 model 的物件對應,我們要來建立兩個 model ,一個查詢用,一個新增用,建一個 src/models.rs 檔案然後輸入以下內容:

#![allow(unused)]
fn main() {
// Insertable 產生的程式碼會使用到,所以必須要引入
use super::schema::posts;

// 一個可以用來查詢的 struct
#[derive(Queryable, Debug)]
pub struct Post {
  pub id: i32,
  pub author: String,
  pub title: String,
  pub body: String,
}

// 新增用的 struct ,唯一的差別是沒有 id 的欄位,以及使用的是 str
#[derive(Insertable)]
// 這邊要指定資料表的名稱,不然 diesel 會嘗試用 struct 的名稱
#[table_name = "posts"]
pub struct NewPost<'a> {
  pub author: &'a str,
  pub title: &'a str,
  pub body: &'a str,
}
}

新增資料

我們先來新增資料,這樣等下才有資料可以查詢,修改 main.rs ,把剛剛建好的 model 引入,接著像這樣輸入 (以下程式碼省略了部份) :

// ...
mod models;

use models::NewPost;

// ...

fn create_post(conn: &SqliteConnection, author: &str, title: &str, body: &str) {
  // 引入我們的資料表
  use self::schema::posts;

  // 建立要準備新增的資料的 struct
  let new_post = NewPost {
    author,
    title,
    body,
  };

  // 指明要新增的表與新的值
  diesel::insert_into(posts::table)
    .values(&new_post)
    // 執行
    .execute(conn)
    .expect("新增貼文失敗");
}

fn main() {
  dotenv().ok();
  let conn = establish_connection();
  // 呼叫 create_post 建立貼文
  create_post(&conn, "Anonymous", "Hello", "Hello world");
}

在執行後你可以找個能打開 sqlite 資料庫的軟體看一下,資料已經確實的新增進去了:

Image

列出資料

我們使用以下程式碼來列出貼文:

// ...

fn list_posts(conn: &SqliteConnection) -> Vec<Post> {
  // 引入資料表的所有東西
  use self::schema::posts::dsl::*;

  // 載入所有的貼文
  posts.load::<Post>(conn).expect("取得貼文列表失敗")
}

fn main() {
  dotenv().ok();
  let conn = establish_connection();
  println!("{:?}", list_posts(&conn));
}

執行後你應該會看到這樣的結果:

[Post { id: 1, author: "Anonymous", title: "Hello", body: "Hello world" }]

正是我們剛剛新增的貼文。

用主鍵查詢資料

// ...
fn get_post(conn: &SqliteConnection, id: i32) -> Post {
  use self::schema::posts::dsl::*;

  // 取得指定 id 的貼文
  posts.find(id).first(conn).expect("取得貼文失敗")
}

fn main() {
  dotenv().ok();
  let conn = establish_connection();
  println!("{:?}", get_post(&conn, 1));
}

刪除貼文

// ...
fn delete_post(conn: &SqliteConnection, id: i32) {
  use self::schema::posts::dsl::*;

  // 不知道你有沒有注意到,除了查詢外的操作都在 diesel 下
  diesel::delete(posts.find(id))
    .execute(conn)
    .expect("刪除貼文失敗");
}

fn main() {
  dotenv().ok();
  let conn = establish_connection();
  delete_post(&conn, 1);
}

大概的使用就這樣,這些應該夠我們建一個留言板了,先把 main 函式清空吧,明天我們再來繼續建立留言板的後端。

用 Rust 做個留言板

這次的程式碼一樣在: https://github.com/DanSnow/rust-intro/tree/master/message-board

今天要來把昨天說好的留言板完成,昨天我們已經建立好了那些用來從資料庫建立、查詢、刪除資料的函式,今天要把它變成網頁後端,請打開昨天的專案,這次要安裝的東西有點多:

$ cargo add serde serde_derive serde_json futures tokio tower-web
  • futures: 提供非同步操作的一個介面
  • tokio: 實作非同步 IO 的函式庫
  • tower-web: 這次要使用的網頁後端框架,它是建立在 tokio 之上的,其實這套是個比較新的框架,在 Rust 的生態系中比較有名的是 rocket ,不過它一定要用到 nightly 的功能,加上我覺得 tower 設計的也挺不錯的。

我們先讓這個後端能送出我們前端的 HTML 吧,你說那個 HTML 嗎?我已經先寫好了,大概的用 bulma 排一下版面而已,沒說很好看,也沒用 React 或 Vue 是用 Vanilla JS 寫,總之你就複製一份到 static/index.html 吧。

總之我們需要一個 struct ,並幫它加上我們的路由:

#![allow(unused)]
fn main() {
// ...
#[macro_use]
extern crate tower_web;
extern crate futures;
extern crate tokio;
use futures::Future;
use std::{env, io, path::PathBuf};
use tokio::fs::File;
// ...
// Clone 是必要的
#[derive(Clone, Copy, Debug)]
struct Service;

// 我們必須把 impl 包在 impl_web 這個 macro 裡
// 這樣 tower-web 才會幫我們產生必要程式碼
impl_web! {
  impl Service {
    // 這代表 get 根目錄的路由
    #[get("/")]
    // 回傳的是 html
    #[content_type("html")]
    // 傳回值是個 Future ,這是由 futures 提供的介面
    // 你可以想像它是個非同步的 Result
    // 所謂的非同步就是程式能先去做別的事,等到這邊好了再回來執行
    // 至於這詳細要怎麼處理,現在不需要去擔心
    fn index(&self) -> impl Future<Item = File, Error = io::Error> + Send {
      // 取專案的根目錄
      let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
      // 加上我們的路徑 static/index.html
      path.push("static");
      path.push("index.html");
      // 打開檔案,這邊使用的是 tokio 提供的 File 不是內建的,要注意喔
      File::open(path)
    }
}
}

然後修改 main 讓它執行:

use tower_web::ServiceBuilder;

fn main() {
  dotenv().ok();

  let addr = "127.0.0.1:8080".parse().unwrap();
  println!("Listen on {}", addr);
  ServiceBuilder::new()
    // 這邊加上剛剛建立的 Service
    .resource(Service)
    .run(&addr)
    .expect("啟動 Server 失敗");
}

編譯執行,在瀏覽器打開 http://localhost:8080 ,應該可以看到網頁顯示出來了,雖然網頁的 console 會顯示錯誤,因為我們還沒實作 API ,如果要關掉 Server 就直接按 Ctrl + C。

接著先來實作取得貼文列表:

#![allow(unused)]
fn main() {
// ...
// 做為回應的型態需要加上 Response
#[derive(Response, Debug)]
struct PostsResponse {
  // 我們的文章列表
  posts: Vec<Post>,
}

impl_web! {
  impl Service {
    // ...
    // 這次是 get /api/posts
    #[get("/api/posts")]
    // 回傳的資料是 json
    #[content_type("json")]
    // 回傳是個 Result ,因為我們沒做錯誤處理
    // 所以 Err 的型態就直接用 () 了
    fn list_posts(&self) -> Result<PostsResponse, ()> {
      // 建立資料庫連線,正常其實不該這樣
      let conn = establish_connection();
      // 取得文章列表後回傳
      Ok(PostsResponse {
        posts: list_posts(&conn)
      })
    }
  }
}
// ...
}

編譯後跑跑看,糟糕,出錯了,我們到 models.rs ,幫我們的 Post 加上 Serialize

#![allow(unused)]
fn main() {
#[derive(Queryable, Serialize, Debug)]
pub struct Post {
    pub id: i32,
    pub author: String,
    pub title: String,
    pub body: String,
}
}

再編譯,這次應該沒錯了,重整網頁應該也不會有錯誤訊息了。

接著來實作新增貼文的 API:

#![allow(unused)]
fn main() {
// ...
// 從用戶端取得資料的 struct 都要實作 Extract
// 另外實作 Extract 的型態裡不能有 borrow
// 這是目前比較可惜的部份
#[derive(Extract, Debug)]
struct CreatePostRequest {
  author: String,
  title: String,
  body: String,
}

#[derive(Response, Debug)]
// 這個是設定當回傳這個型態時 http status code 用 201
#[web(status = "201")]
struct CreatePostResponse {
  ok: bool,
}

impl_web! {
  impl Service {
    // ...

    #[post("/api/posts/create")]
    #[content_type("json")]
    // body 是從 post body 取得的
    // 另外也可以把變數取名叫 query_string 就可以從 query string 拿到資料
    // 當然這都是 impl_web 這個 macro 提供的功能
    fn create_post(&self, body: CreatePostRequest) -> Result<CreatePostResponse, ()> {
      let conn = establish_connection();
      create_post(&conn, &body.author, &body.title, &body.body);
      Ok(CreatePostResponse { ok: true })
    }
  }
}
}

重新編譯執行,我們應該要能發佈貼文了,同時發佈後貼文就會更新,到這邊就完成了一個不能修改也不能刪除的留言板了,我們再來把剩下的 CRUD (Create、Retrieve、Update、Delete 建立、取得、更新、刪除) …,沒有 U ,來給完成吧。

#![allow(unused)]
fn main() {
#[macro_use]
extern crate serde_json;
// ...
#[derive(Response, Debug)]
struct PostResponse {
    post: Post,
}

impl_web! {
  impl Service {
    // ...
    // 網址中的變數是使用 : 來宣告的
    #[get("/api/posts/:id")]
    #[content_type("json")]
    // 這邊的變數要跟上面的同名
    fn get_post(&self, id: i32) -> Result<PostResponse, ()>{
      let conn = establish_connection();
      Ok(PostResponse { post: get_post(&conn, id) })
    }

    #[delete("/api/posts/:id")]
    // 回傳的是 json 型態,如果回傳的是 serde_json::Value
    // 就會自動用 json 回傳了
    fn delete_post(&self, id: i32) -> Result<serde_json::Value, ()>{
      let conn = establish_connection();
      delete_post(&conn, id);
      // serde_json 有提供 json 這個 macro 來建立 json
      // 只是用這個方法沒辦法自訂 http status code
      Ok(json!({
        "ok": true
      }))
    }
  }
}
// ...
}

好了,然後前端再加個刪除的按鈕就行了,但是那個我就不做了,用 Rust 寫網頁後端最大的好處應該就是執行速度快了,每次修改都要編譯再執行其實挺麻煩的,如果不是真的有很嚴重的效能問題不然還是用其它的語言寫比較方便,這個就當一個嘗試吧。

明天就來介紹一下 tokio 與 futures 是怎麼一回事。

不知不覺得這個系列已經到第 26 篇了,如果有什麼覺得有趣的東西想知道怎麼用 Rust 做的歡迎提出來喔,或是有哪個東西希望我介紹的,因為老實說,我快沒點子了。

非同步 IO : Futures 與 Tokio

Async IO

在開始之前要先來介紹一下非同步的 IO 是怎麼回事,如果你有碰過比較底層的系統程式,你可能會知道在 Linux 下用來開啟檔案的 open 有個選項是 O_NONBLOCK ,它會做什麼?它做的事就是當你讀取資料時,若資料還沒準備好,讀取的操作就會直接以失敗返回,而不會在那邊等到有資料,於是在這段時間內程式就能去做其它的事,晚點再回來試著讀看看,而且這並不只可以用在檔案上, Linux 系統把非常多的東西都視為檔案去操作,這包含網路連線,與硬體的溝通等等,當然 Windows 下也有類似的機制。

當然,盲目的嘗試其實很沒效率,所以作業系統也提供了方法可以讓我們偵測是不是可以讀取了,我們可以開啟多個檔案、連線或裝置,等到系統通知我們其中有某個已經準備好能讀取了再去處理,這就是所謂的事件驅動 (event driven) ,同時也是像 Nodejs 內部的處理方式。

這樣的方式能以單一的執行緒處理所有的讀寫操作,比起原本的同步 IO 必須使用多個執行緒或處理序省下了切換行程等等的成本,但相對的會讓程式的設計變得複雜,不過理想上這些處理非同步 IO 的框架可以幫我們把程式碼變的像同步一樣。

Futures

futures 是 Rust 社群所提出的一個統一非同步操作的一個介面,大家可以實作一個統一的介面去描述一個尚未完成的事物,之前也有說過它像非同步的 Result ,它用起來像這樣:

extern crate futures;

use futures::{future, Future};

fn main() {
  // future::ok 會直接建一個完成,並且有結果的 Future
  let fut = future::ok::<i32, ()>(111).map(|x| x * 3);
  // wait 會執行一個 Future ,並等到它完成
  assert_eq!(fut.wait().unwrap(), 333);
}

如果你用過 Javascript 你可以想像 future::ok 就是 Promise.resolve ,不過它還有附很多操作的方法

其實目前在網站上看到的會是 0.3 的 alpha 版,但常用的卻是 0.1 版的,目前 futures 將會有部份整合進 Rust 的標準函式庫中,而之後 Rust 預計要支援 async 與 await , 0.3 版的則是要跟 async 與 await 搭配使用的,若支援的話就能把非同步的程式碼寫的像同步的一樣:

#![feature(async_await, await_macro)]
extern crate futures;

use futures::future;
use futures::executor::block_on;

fn main() {
  block_on(async {
    let n = await!(future::ok::<i32, ()>(111)).unwrap();
    assert_eq!(n, 111);
  });
}

這邊要使用的 futures 是 futures-preview = "0.3.0-alpha.4"

你可以在 std::futurestd::pin 找到那些整合進來的 API ,當然目前都還是不穩定的。

Tokio

Tokio 則是實作了非同步 IO 的框架,它提供了經過包裝的非同步的檔案與網路操作,同時還提供了執行 Future 的功能。

說來執行 Future 是怎麼一回事,事實上 Future 的實作就是透過輪詢 (poll) 來確定 Future 完成了沒,若還沒就讓它繼續跑,好了的話就把結果拿出來,所以若要自己來做這個工作就會變成:

extern crate futures;

use futures::{future, Async, Future};

fn main() {
    let mut fut = future::ok::<i32, ()>(111).map(|x| x * 3);

    loop {
      match fut.poll() {
        Ok(Async::Ready(res)) => {
          println!("{}", res);
          break;
        }
        Ok(Async::NotReady) => {
          println!("Not ready");
        }
        Err(e) => panic!(e),
      }
    }
}

記得把 futures 換回原本的 0.1 版

不要懷疑,這個是可以跑的,當然上面這個情況不可能會出現 Not ready 就是了。

所以事實上如果要執行一個 Future 問題就變成了,如果遇到了 Async::NotReady 程式是不是可以等一下,等到有結果了再做一次輪詢,而這個等一下又要等多久,而 tokio 的解決辦法就是使用系統底層的 API 實作 IO 的通知機制,若 futures 是從 IO 操作建立的就向系統註冊需要取得狀態改變的通知,而在有通知時再做一次輪詢。

這部份可能比較不好懂,但我想避免直接講到系統底層的 API ,如果有興趣可以看看 Linux 下的 epoll,還有 Rust 的 mio , mio 正是 tokio 的核心, tokio 使用 mio 來包裝這些底層 API ,而 mio 則在 Linux 下使用 epoll 實作通知機制

若我們用 tokio 寫一個 echo server 的話會像這樣:

extern crate futures;
extern crate tokio;

use futures::{Future, Stream};
use std::net::SocketAddr;
use tokio::{
    io::{self, AsyncRead},
    net::TcpListener,
    runtime::current_thread::Runtime,
};

fn main() {
  let mut runtime = Runtime::new().unwrap();
  let handle = runtime.handle();
  let listener = TcpListener::bind(&SocketAddr::new("127.0.0.1".parse().unwrap(), 1234)).unwrap();
  let fut = listener
    .incoming()
    .for_each(|stream| {
      let (read, write) = stream.split();
      // 若要用 tokio::run 這邊要換成 tokio::spawn
      handle
        .spawn(
          io::copy(read, write)
            .map(|_| ())
            .map_err(|e| println!("{:?}", e)),
        )
        .unwrap();
      Ok(())
    })
    .map_err(|e| println!("{:?}", e));
  runtime.block_on(fut).unwrap();
  // 這邊其實使用 tokio 讓它用預設的方法執行 Future 會比較方便
  // 只是預設的方法會產生一些 thread ,我希望這邊可以用單執行緒完成
  // tokio::run(fut);
}

在 Linux 下你可以用 nc , Windows 下可以用 telnet 來連線到 localhost:1234 ,你輸入什麼伺服器就會回應你什麼,這樣程式看起來其實不難,底層也變成使用事件驅動的方式執行。

上面的程式碼我放在: https://github.com/DanSnow/rust-intro/tree/master/echo-server

我個人是認為 tokio 與 futures 它們的 API 都很抽像,老實說不好理解,不過也有很多建立在這兩個函式庫上的東西幫它們做了很好的包裝,讓你可以解決特定領域的問題,比如像昨天所介紹的 tower-web ,它就讓你不需要去碰到 tokio 的細節部份。

另外如果想了解更多關於 tokio 與 mio 的底層的人可以參考這篇翻譯文章: 【譯】Tokio 內部機制:從頭理解 Rust 非同步 I/O 框架

WebAssembly: 用 Rust 寫出高效能的網頁程式

什麼是 WebAssembly

WebAssembly 是個實驗性的 Web 標準,其是由其它程式語言如 C/C++ ,目前 Go 也支援了,當然還有我們的 Rust ,由這些語言來編譯成位元組碼 (bytecode) 來執行,並提供比 js 還要來的更快的,接近原生程式的效能。

Rust 與 WebAssembly

Rust 原生支援編譯成 WebAssembly,你只需要底下這行指令就能安裝這項功能:

$ rustup target add wasm32-unknown-unknown

接著我們來安裝 wasm-bindgen 這個工具:

$ cargo install wasm-bindgen-cli

這個工具是之後來幫我們最佳化編譯出來的 wasm 檔與產生對應的 js 包裝用的。

這次的專案在: https://github.com/DanSnow/rust-intro/tree/master/wasm-demo

我們可以來開個小專案測試一下了:

$ cargo init --lib wasm-demo
$ cd wasm-demo
$ cargo add wasm-bindgen web-sys
  • wasm-bindgen: 自動產生 JS 與 Rust 的綁定
  • js-sys: js 的基本型態等等,這邊並沒有用到
  • web-sys: 網頁的 API 綁定

然後我們需要編輯一下 Cargo.toml ,首先加上這段:

[lib]
crate-type = ["cdylib"]

然後編輯 web-sys 的相依性:

web-sys = { version = "0.3.2", features = ["Window", "Document", "Node", "HtmlElement", "Element"] }

這個有點長啊,我建議你改寫成這個型式:

[dependencies.web-sys]
version = "0.3.2"
features = ["Window", "Document", "Node", "HtmlElement", "Element"]

web-sys 為了避免編譯不需要的功能,所以用了一大堆的 feature ,使用的時候最好看一下 文件 確定一下自己要的功能要開啟哪個 feature

之後還要加上一個設定檔在 .cargo/config 內容是:

[build]
target = "wasm32-unknown-unknown"

這讓 cargo 預設把這個專案編譯成 WebAssembly 。

接著我們會需要 webpack 來幫我們打包 wasm 與 js 的 code ,所以這邊就假設你已經有 Node.js 的環境了,如果沒有的話要去安裝。

$ npm init --yes
$ npm install --dev webpack webpack-cli webpack-dev-server html-webpack-plugin shelljs

或是像我一樣使用 yarn 也行:

$ yarn init --yes
$ yarn add -D webpack webpack-cli webpack-dev-server html-webpack-plugin shelljs

我們先開始寫程式碼吧,修改 src/lib.rs 如下:

extern crate wasm_bindgen;
extern crate web_sys;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn main() -> Result<(), JsValue> {
  // 取得 window 物件
  let window = web_sys::window().unwrap();
  // 取得 document 物件,以下類推
  let document = window.document().unwrap();
  let body = document.body().unwrap();
  // 建立 HTML 元素
  let el = document.create_element("p")?;
  // 設定裡面的字
  el.set_inner_html("Hello from Rust");
  // 這邊必須要使用 AsRef::<web_sys::Node>::as_ref(&body) 這種寫法
  // 因為這邊還沒辦法很好的做型態的轉換
  // 轉成 Node 型態才有 append_child 能用
  // 而 el.as_ref() 也是在做型態轉換,只是這邊就能自動推導
  AsRef::<web_sys::Node>::as_ref(&body).append_child(el.as_ref())?;
  Ok(())
}

然後再寫個 index.js

// wasm 的模組不允許同步的載入,所以要用 import()
const wasm = import('./wasm_demo')

wasm
  .then(m => {
    // 執行上面的 main
    m.main()
  })
  .catch(console.error)

然後接下來就是需要編譯與打包了,編譯的指令如下:

$ cargo build --release
$ wasm-bindgen target/wasm32-unknown-unknown/release/wasm_demo.wasm --out-dir .

在 build 完後你會發現檔案是被放在 wasm32-unknown-unknown/release 下,這是因為我們設定編譯成 wasm ,而因為這並不是預設的就被放到別的資料夾了。

接著的 wasm-bindgen 指令幫我們最佳化 wasm 檔案並產生一份 js 檔,輸出到現在的資料夾下。

我也把這個步驟寫成了 build.js ,你可以使用 node build.js 執行。

之後就是要打包了,請你直接把 webpack.config.js 複製過去,然後執行:

$ npm run webpack-dev-server

或用 yarn

$ yarn webpack-dev-server

這會開啟開發用的伺服器,你可以打開瀏覽器連上 http://localhost:8080 應該會看到 Hello from Rust

Port message-board 到 WebAssembly

這個專案在: https://github.com/DanSnow/rust-intro/tree/master/message-board-wasm

再來個比較複雜點的專案,這次挑戰把之前的留言板的 js 部份全部用 Rust 改寫,先開個新專案:

$ cargo init --lib message-board-wasm
$ cd message-board-wasm
$ cargo add wasm-bindgen web-sys js-sys

一些細部的設定與重覆的部份我就省略了,底下我也只挑重點節錄,有興趣自己開專案來看:

// ...
fn render_posts(posts: Posts) -> Result<JsValue, JsValue> {
    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();
    let fragment = document.create_document_fragment();
    let frag_node = AsRef::<web_sys::Node>::as_ref(&fragment);
    for post in posts.posts.iter().rev() {
        let card = document.create_element("div").unwrap();
        card.set_class_name("card");
        card.set_inner_html(&format!(
            r#"<div class="card-content">
            <p class="title is-5">{}</p>
            <p class="subtitle is-5">{}</p>
            <div class="content">
              {}
            </div>
          </div>"#,
            post.title, post.author, post.body
        ));
        frag_node.append_child(card.as_ref())?;
    }
    let container = document.query_selector(".posts")?.unwrap();
    container.set_inner_html("");
    AsRef::<web_sys::Node>::as_ref(&container).append_child(fragment.as_ref())?;
    Ok(JsValue::UNDEFINED)
}

fn fetch_posts(window: &web_sys::Window) {
  // JsFuture 會把 Promise 轉成 Future
  // future_to_promise 則是做相反的事
  future_to_promise(
    JsFuture::from(window.fetch_with_str("/api/posts"))
      .and_then(|res: JsValue| res.dyn_into::<web_sys::Response>().unwrap().json())
      // 這邊要再轉換 json 所回傳的 Promise
      .and_then(|json: js_sys::Promise| JsFuture::from(json))
      // parse json 後顯示
      .and_then(|json| render_posts(json.into_serde::<Posts>().unwrap())),
  );
}

fn handle_submit(event: web_sys::Event) {
  // 前面都在取表單的值
  // ...
  let data = JsValue::from_serde(&Post {
    author,
    title,
    body,
  })
  .unwrap();
  web_sys::console::log_1(&data);
  let req = web_sys::Request::new_with_str_and_init(
    "/api/posts/create",
    web_sys::RequestInit::new()
      // 或許這邊用 serde_json 會比較好點
      .body(Some(js_sys::JSON::stringify(&data).unwrap().as_ref()))
      .method("POST"),
  )
  .unwrap();
  req.headers()
    .set("Content-Type", "application/json")
    .unwrap();
  let window = web_sys::window().unwrap();
  let cb = Closure::wrap(Box::new(|_| {
    let window = web_sys::window().unwrap();
    fetch_posts(&window);
  }) as Box<FnMut(_)>);
  window.fetch_with_request(&req).then(&cb);
  // 同下
  cb.forget();
}

#[wasm_bindgen]
pub fn main() -> Result<(), JsValue> {
  // 這個會讓 debug 方便點,它讓 panic 時印出 stack trace
  console_error_panic_hook::set_once();
  let window = web_sys::window().unwrap();
  let document = window.document().unwrap();
  let create_post = document.query_selector("#create-post")?.unwrap();
  // 這是 console.log
  web_sys::console::log_1(create_post.as_ref());

  // 這邊一定要做轉型, Closure::wrap 要的是一個內部的型態
  let handler = Closure::wrap(Box::new(handle_submit) as Box<Fn(_)>);
  AsRef::<web_sys::EventTarget>::as_ref(&create_post)
    // 這邊要做兩次轉型,一次 as_ref 一次 unchecked_ref 才能拿到 js_sys::Function
    .add_event_listener_with_callback("submit", handler.as_ref().unchecked_ref())?;
  // 一定要呼叫 Closure 的 forget ,或是把它回傳,不然 Rust 這邊會把它釋放掉
  handler.forget();
  fetch_posts(&window);
  Ok(())
}

這樣就完成了一個前端是 Rust 後端還是 Rust 的網站了,可惜的是我還沒找到方法讓之前做的後端能夠送出正確的 mime 的方法,而 Chrome 會因為 mime 不對而拒絕執行 wasm ,所以我是用 http-server 當伺服器的部份, API 的部份則用 Proxy 轉發給原本的後端處理。

Raw String

上面使用到了一個語法:

#![allow(unused)]
fn main() {
r#"有 "" 也沒問題"#
}

這是 Rust 裡的 raw string ,它的語法是 r 後跟著 1 個以上的 # 再接 " ,之後若不是碰到 " 後接著同樣數量的 # 都不會當做結束,這中間出現 " 也沒問題,所以底下的也是可以的:

#![allow(unused)]
fn main() {
r##" 這次出現 #""# 也行"##
}

使用者可以根據情況調整 # 的數量。

後記

事實上像留言板的程式改寫成 WebAssembly 不一定會比較快,我個人是覺得 WebAssembly 目前應該著重於運算量大的,或是像 js 所不擅長處理的 64 位元整數,目前的 WebAssembly 其實很多東西都沒辦法直接操作,這中間靠了很多 webpack 與 wasm-bindgen 的處理把 js 與 wasm 整合起來,不然正常情況下 wasm 這邊是沒辦法直接處理 dom 的,這次算一個挑戰,除錯到很崩潰,差點要放棄,我一開始並沒有用 console error 的那個 crate ,結果沒想到真的遇到了 panic ,後來查了才發現,我把型態記錯了,我把 HtmlTextAreaElement 轉型成 HtmlInputElement,不過還是順利的把這篇文章生出來了。

用 Rust + GTK 做個井字棋

前幾天有看到別人鐵人賽在做井字棋,我也來做一個吧,用 Rust ,加上 GTK ,做成原生的視窗程式。

但在開始前 Rust 最近發佈了 1.30 版,這版裡有些有趣的功能像 procedural macro ,它是讓你可以用 Rust 的程式處理 Rust 的抽像語法樹 (AST) ,然後去改變或產生程式,這其實已經不是 macro 了,而是 meta programming 了,然後還有 crate 的 macro 可以用 use 載入了,而不用 #[macro_use] (意思是自己寫的還是要), Rust 2018 的功能開始出現了,下一版就會是第一版的 Rust 2018 了,想看詳細內容可以去看 官方 blog

GTK 是什麼

GTK 是個在類 Unix 系統下的一個主流的圖形介面程式開發的函式庫,可以讓你做出有視窗的程式,而不是終端機那黑黑的畫面,雖然它是用 C 寫的,但非常的 OOP ,而提供 Rust 綁定的函式庫: gtk-rs ,也很好的把原本的繼承關係等等的用 Rust 的 trait 實作出來了。

它實際上由數個部份組成:

  • gtk: 最上層的函式庫,提供圖形的元件
  • gdk: gtk 的繪圖函式庫,也處理字型、遊標等等
  • gio: 處理 IO 與檔案等等的操作
  • glib: 最低層的函式庫,提供資料結構、常用函式與物件系統

另外還有 cario 與 pango 等。

建立專案

這次的專案在: https://github.com/DanSnow/rust-intro/tree/master/gtk-tic-tac-toe

不多說了:

$ cargo init gtk-tic-tac-toe
$ cd gtk-tic-tac-toe
$ cargo add gtk gio gdk

然後我們要修改一下 Cargo.toml

gtk = { version = "0.5.0", features = ["v3_22"] }

後面的 v3_22 要配合電腦上有的 gtk 函式庫的版本調整,我所使用的是 3.22 ,另外電腦上也要裝 gtk 的開發用函式庫, Ubuntu 下套件的名字是 libgtk-3-dev

Hello in GTK

修改 src/main.rs

extern crate gdk;
extern crate gio;
extern crate gtk;

use gio::prelude::*;
use gtk::prelude::*;

fn main() {
  let application = gtk::Application::new(
    // 這邊要放個 id
    "io.github.dansnow.tic-tac-toe",
    // 不需要什麼選項
    gio::ApplicationFlags::empty(),
  )
  .expect("建立 APP 失敗");
  // 設定程式啟動時的動作,這邊的 app 實際上是 application 的 borrow
  application.connect_startup(|app| {
    // 建立視窗
    let window = gtk::ApplicationWindow::new(app);
    // 設定標題
    window.set_title("Tic Tac Toe");
    // 預設視窗大小
    window.set_default_size(300, 200);
    // 設定按叉叉時的動作
    window.connect_delete_event(move |win, _| {
      // 關掉視窗
      win.destroy();
      Inhibit(false)
    });
    let label = gtk::Label::new("Hello");
    window.add(&label);
    // 顯示視窗
    window.show_all();
  });
  application.connect_activate(|_| {});
  // 程式啟動
  application.run(&args().collect::<Vec<_>>());
}

執行看看,應該會有個視窗出現,想當初第一次接觸視窗程式,第一次脫離那黑色的視窗時有多感動 (然後我就寫了個全螢幕但沒有離開的方法的程式丟在別人的電腦上) 。

建立程式畫面

這次的目標長這樣:

Image

其實沒很好看,我盡力了。

盤面的部份每個都是一個按鈕,用的是 GtkToggleButton,三個包進一個橫的 GtkBox (這是 GTK 裡很常用的一個排版元件) ,最上面是 GtkToolbar ,最後再全部包進一個直的 GtkBox,總之程式碼是這樣子的:

#![allow(unused)]
fn main() {
fn setup_ui(app: &gtk::Application) {
  // 建立 window
  // ...

  // 這是遊戲的邏輯部份,等下再講
  let game = Rc::new(RefCell::new(Game::new()));
  // 最外層的 Box
  let outer_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
  // 按鈕的圖示
  let icon = gtk::Image::new_from_icon_name("restart", 16);
  // 工具列
  let toolbar = gtk::Toolbar::new();
  // 工具列的按鈕
  let tool_button = gtk::ToolButton::new(Some(&icon), None);
  // 把按鈕加到工具列
  toolbar.insert(&tool_button, 0);
  {
    let game = game.clone();
    // 設定重置按鍵按下時重置遊戲
    tool_button.connect_clicked(move |_| {
        game.borrow_mut().reset();
    });
  }
  // 把 Toolbar 加進去 outer_box
  outer_box.pack_start(&toolbar, true, false, 0);
  for y in 0..3 {
    let inner_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
    for x in 0..3 {
      let button = gtk::ToggleButton::new();
      // 我們之後需要重置這些按鈕的狀態,所以把它存起來
      game.borrow_mut().push_button(button.clone());
      // 這是自訂的 macro
      clone!(game, window);
      // 設定按鈕狀態改變時的動作
      button.connect_toggled(move |btn| {
        // 因為重置按鈕時也會觸發,所以要檢查
        if !btn.get_active() {
          return;
        }
        // 停用按鈕,這樣使用者就不能再按了
        btn.set_sensitive(false);
        let p = game.borrow().player;
        // 插入 O 或 X 的符號
        let label = gtk::Label::new(None);
        label.set_markup(p.markup());
        btn.add(&label);
        label.show();
        // 放棋與檢查
        if game.borrow_mut().place_and_check((x, y)) {
          show_message(&window, p.label());
        }
      });
      // 把按鈕加建 box
      inner_box.pack_start(&button, true, true, 0);
    }
    outer_box.pack_start(&inner_box, true, true, 0);
  }
  window.add(&outer_box);
  window.show_all();
}
}

建立 UI 就是這麼麻煩,雖然有個工具叫 Glade 可以用拉的,不過這次我並沒有使用。

修改元件的外觀

如果程式碼是像上面那樣,那建出來的畫面會是:

Image

這配色是我電腦的主題,平常很好看,但在這邊不適合,所以我們來寫點 CSS 修改它,建一個 src/style.css

.toggle {
  background-color: lightseagreen;
  background-image: none;
}

.toggle label {
  color: black;
  font-size: 1.5em;
}

對,別懷疑,就是寫網頁用的 CSS , GTK 的主題設定是使用 CSS 的,順帶一提,你可以用以下指令:

$ GTK_DEBUG=interactive cargo run

程式打開始還會開啟一個圖形介面用的除錯器:

Image

就像 Chrome 的 F12 一樣,而且 GTK_DEBUG=interactive 可以用在所有使用 gtk3 的程式上。

準確來說 gtk3 的主題才是使用 CSS , gtk2 是使用自訂的語言,另外比較早的 gtk3 版本是使用元件名稱當成 css selector 的,像 GtkButton ,現在則是改用跟網頁元素類似的名稱,或是使用 class 了,像 button.toggle 就是指 GtkToggleButton

接著我們要載入 CSS :

#![allow(unused)]
fn main() {
// include_bytes! 可以把相對於原始碼目錄的檔案以 &[u8] 載入,並編譯到程式裡
// 另外還有個 include_str! 則是載入成字串
const STYLE: &[u8] = include_bytes!("style.css");

fn setup_ui(app: &gtk::Application) {
  let screen = gdk::Screen::get_default().unwrap();
  let provider = gtk::CssProvider::new();
  provider.load_from_data(STYLE).expect("載入 css 失敗");
  gtk::StyleContext::add_provider_for_screen(
    &screen,
    &provider,
    gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
  );
  // ...
}
}

顯示訊息

這邊很簡單的顯示了個對話框做為輸贏的提示:

Image

#![allow(unused)]
fn main() {
fn show_message(window: &gtk::ApplicationWindow, winner: &str) {
  let dialog = gtk::MessageDialog::new(
    Some(window),
    // 對話框選項
    gtk::DialogFlags::MODAL | gtk::DialogFlags::DESTROY_WITH_PARENT,
    // 對話框類型
    gtk::MessageType::Info,
    // 對話框上的按鈕
    gtk::ButtonsType::Close,
    // 格式化訊息
    &format!("{} Win", winner),
  );
  // 這邊設定按下按鈕後做的動作
  dialog.connect_response(|dialog, _| {
    dialog.destroy();
  });
  dialog.show_all();
}
}

遊戲邏輯

#![allow(unused)]
fn main() {
// PartialEq 與 Eq 是讓它可以判斷是否相等
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum Player {
  // 圈
  Circle,
  // 叉
  Cross,
}

impl Player {
  // 取得對手
  fn opponent(&self) -> Self {
    match self {
      Player::Circle => Player::Cross,
      Player::Cross => Player::Circle,
    }
  }

  // 取得文字的表示
  fn label(&self) -> &'static str {
    match self {
      Player::Circle => "O",
      Player::Cross => "X",
    }
  }

  // 給 Label 用的
  fn markup(&self) -> &'static str {
    match self {
      Player::Circle => r#"<span color="white">O</span>"#,
      Player::Cross => "X",
    }
  }
}

// 一開始從圈圈開始
impl Default for Player {
    fn default() -> Self {
        Player::Circle
    }
}

#[derive(Default, Debug)]
struct Game {
    pub player: Player,
    board: [[Option<Player>; 3]; 3],
    buttons: Vec<gtk::ToggleButton>,
}

impl Game {
  // ...

  // 檢查輸贏的函式,只需要檢查放棋的點的上下左右與鈄的就行了
  fn check(&self, target: (i32, i32)) -> bool {
    let (px, py) = target;
    let (px, py) = (px as usize, py as usize);
    // 檢查直的
    if self.board[0][px] == self.board[1][px] && self.board[0][px] == self.board[2][px] {
      return true;
    }
    // 橫的
    if self.board[py][0] == self.board[py][1] && self.board[py][0] == self.board[py][2] {
      return true;
    }
    // 中間沒放就不會有鈄的情況了
    if self.board[1][1].is_none() {
      return false;
    }
    // 左上到右下鈄
    if self.board[0][0] == self.board[1][1] && self.board[0][0] == self.board[2][2] {
      return true;
    }
    // 右上到左下鈄
    if self.board[0][2] == self.board[1][1] && self.board[0][2] == self.board[2][0] {
      return true;
    }
    false
  }

  // ...
}
}

你說沒有 AI 嗎?對,這是個單機雙人遊戲,你可以找朋友一起玩,或是自己跟自己玩。

說來寫到這段時我真的覺得 Rust 能幫列舉加上方法的這個功能真的很好用。

macro

這次用了個自訂的 macro:

#![allow(unused)]
fn main() {
macro_rules! clone {
  ($($name:ident),*) => {
    $(
      let $name = $name.clone();
    )*
  };
}
}

這個 macro 會把傳進去的變數都 clone 一份後再賦值給同名的變數, macro 的詳細的寫法請讓我留到明天再講,我有記得我說過要講解這個喔,絕對沒忘喔。

macro 、 proc macro 與尾聲

這是最後一篇了,這篇我會補一些之前沒講到的東西,內容會比較雜一點。

目錄:

  • macro
  • Procedural Macro (進階)
  • 權限修飾
  • Rust 心得
  • 鐵人賽後記

macro

Rust 的 macro 同樣也是基於模式比對的,如果你發現了類似結構的程式碼重覆了,說不定你可以把它寫成 macro 來減少你的程式碼,比如像昨天使用的 clone!

#![allow(unused)]
fn main() {
macro_rules! clone {
  ($($name:ident),*) => {
    $(
      let $name = $name.clone();
    )*
  };
}
}

定義一個 macro 你需要使用 macro_rules! 接著你想要的 macro 名稱,名稱的規則跟變數一樣,這邊使用的是 clone ,之後你的 macro 就會變 clone!

接下來則是分成兩個部份,模式定義的部份與產生的程式碼,兩邊都必須使用成對的括號包住,中間使用 => 分隔,最後則要用 ; 結束 (只有一個定義時可以不用),左邊的模式幾乎可以隨便定義,只是還是有些規則在,主要是不能造成編譯器出現岐義的情況,也就是同一段程式碼能有兩種不同的解釋,像 macro 中能用 expr 去比對運算式,但是運算式後就規定不能出現運算子 (+ 、 - 、 * 、 / 等等) 。

上面的 $name:ident 代表的是比對一個識別字 (變數或 struct 的名字等等),前面的 $name 是 macro 中使用的變數,在展開後 $name 會被代換成傳入的東西,後面的 ident 則代表是要比對識別字,可以比對的東西有這些:

  • item: 各式各樣的完整定義與宣告都是 item ,比如一個 struct 的定義
  • block: 一個程式碼區塊,就是由一對 {} 包起來的區塊
  • stmt: 一個程式敘述,就是一行有加 ; 的程式碼
  • pat: 一個模式比對用的模式,如 Point { x, y }
  • expr: 一個運算式,基本上就是一行沒加括號有回傳值的程式,這個挺常用的
  • ty: 一個型態
  • ident: 一個識別字,或一個關鍵字也行,這個也是常用的
  • path: 比如像 std::vec::Vec 這樣的
  • meta: #[] 中的內容
  • lifetime: 就是 lifetime
  • vis: 像 pub 這樣的設定可見性的東西
  • tt: 可以 match 任何東西,通常是用來收集還沒比對到的部份用的

比如我可以定義:

#![allow(unused)]
fn main() {
macro_rules! foo {
  ($left:ident =>____<= $right:expr) => {
    println!("{} =>____<= {}", stringify!($left), $right);
  };
}

// 使用:

foo!(bar =>____<= 123);
// 會印出: "bar =>____<= 123"
}

其中 stringify! 是個內建的 macro ,可以把傳進去的東西原封不動的轉成字串,沒錯, macro 可以遞迴展開。

如果要重覆的比對的話就要用 $()*$()+ 了, + 是比對 1 個以上,而 * 則是可以為 0 個,如果我們把上面的範例加上 $()*

#![allow(unused)]
fn main() {
macro_rules! foo {
  ($($left:ident =>____<= $right:expr)*) => {
    $(
      println!("{} =>____<= {}", stringify!($left), $right);
    )*
  };
}

// 使用:

foo!(bar =>____<= 123 baz =>____<= 456);
// 會印出:
// bar =>____<= 123
// baz =>____<= 456
}

用哪種重覆的方式就要用哪種去展開,在結尾的 *+ 前可以放上分隔用的符號,通常用會 ,;

#![allow(unused)]
fn main() {
macro_rules! foo {
  ($($left:ident =>____<= $right:expr),*) => {
    $(
      println!("{} =>____<= {}", stringify!($left), $right);
    )*
  };
}

// 使用:

foo!(bar =>____<= 123, baz =>____<= 456);
// 會印出:
// bar =>____<= 123
// baz =>____<= 456
}

Rust 的 macro 很強大,又能遞迴展開,使用的好能做出各式各樣的效果,比如:

#![allow(unused)]
fn main() {
macro_rules! foo {
  // 遞迴的終止條件
  ($left:ident -> $right:expr) => {
    println!("{} -> {}", stringify!($left), $right);
  };

  // 遞迴的終止條件
  ($left:ident => $right:expr) => {
    println!("{} => {}", stringify!($left), $right);
  };

  ($left:ident -> $right:expr, $($rest:tt)+) => {
    println!("{} -> {}", stringify!($left), $right);
    foo!($($rest)+);
  };

  ($left:ident => $right:expr, $($rest:tt)+) => {
    println!("{} => {}", stringify!($left), $right);
    foo!($($rest)+);
  };
}
}

這樣就能支援兩種不同的分隔。

也有人在 macro 裡做出狀態機:

#![allow(unused)]
fn main() {
macro_rules! foo {
  ($left:ident => $right:expr) => {
    foo!(@end $left => $right);
  };

  ($left:ident => $right:expr, $($rest:tt)+) => {
    println!("{} => {}", stringify!($left), $right);
    foo!(@second $($rest)+);
  };

  (@second $left:ident => $right:expr, $($rest:tt)+) => {
    println!("{} => {}", stringify!($left), $right);
    foo!(@end $($rest)+);
  };
  
  (@end $left:ident => $right:expr) => {
    println!("{} => {}", stringify!($left), $right);
  };

  (@end $left:ident => $right:expr, $($rest:tt)*) => {
    println!("{} => {}", stringify!($left), $right);
  };
}
}

這會讓只能比對到 1 組或 3 組以上,而且超過第 3 組後都會被忽略,至於使用 @ 只是個慣例,加上基本上程式裡不會出現。

另外還可以搭配 trait 來實作一些針對型態的特化等等的。

Procedural Macro (進階)

這個功能就是像 derive 所使用的 trait ,或是可以自訂屬性 (像 #[foo]) ,社群有提供兩個很好用的 crate 可以幫忙實作,分別是 synquote

這個專案在: https://github.com/DanSnow/rust-intro/tree/master/proc-macro-demo

假設我們有個 trait 是定義一個函式回傳 struct 的名字:

#![allow(unused)]
fn main() {
trait Name {
  fn name() -> &'static str;
}
}

然後我們把它變成可以 derive 的,於是我們必須建一個額外的函式庫專案,並在 Cargo.toml 中加上:

[lib]
proc-macro = true

這樣編譯器才會知道這個 crate 是 proc macro 然後實作的程式碼如下:

#![allow(unused)]
fn main() {
#[macro_use]
extern crate quote;
extern crate proc_macro;
extern crate proc_macro2;
extern crate syn;

use self::proc_macro::TokenStream;

// 定義一個 derive 名稱為 Name
#[proc_macro_derive(Name)]
// 這個函式的輸入輸出是規定的
pub fn name_derive(input: TokenStream) -> TokenStream {
    let ast = syn::parse(input).unwrap();
    impl_name(&ast).into()
}

fn impl_name(ast: &syn::DeriveInput) -> proc_macro2::TokenStream {
  // 取得 struct 的名稱
  let name = &ast.ident;
  // 轉成字串,這樣才會在 quote 裡是 "<名稱>" 的型式
  let name_str = name.to_string();
  quote! {
    impl ::name::Name for #name {
      fn name() -> &'static str {
        #name_str
      }
    }
  }
}
}

使用像這樣:

#[macro_use]
extern crate name_derive;
extern crate name;

use name::Name;

#[derive(Name)]
struct Foo;

fn main() {
  println!("{}", Foo::name());
}

權限修飾

之前提過用 pub 可以公開模組中的東西,事實上並不單只有這樣的用法:

  • pub(crate): 讓這個東西能在這個 crate 中使用,但離開這個 crate 就不能
  • pub(in <模組路徑>): 只再開放給指定的模組,路徑也可以用 selfsuper

此外也可以把 pub(in super) 寫成 pub(super)self 也同樣,不過其實寫 pub(self) 就相當於是預設的,還有路徑只能是上層的路徑,下層的原本就都可以存取的。

使用 Rust 的心得

當初知道 Rust 這個語言是因為火狐說要用這個語言重寫他們的引擎,那時就去查了一下,記得那個時候的第一印像是:「這什麼鬼, struct 跟 impl 的定義是分開的?!」,不過後來嘗試後慢慢發現,這個語言雖然是個系統程式語言,但寫起來卻很方便,我在那之前主要使用的是 C++ ,接觸 Rust 後我最喜歡的是它的型態推導 (這在 C++ 裡可以用 auto),跟它的模式比對 (這遠遠超過了 C++ 的 switch 了),還有一些 FP 的特性,這是在 C++ 中比較缺乏,最重要的是 Rust 有強大的社群支援,還有 cargo 這個套件管理工具, C++ 若想用一些第三方的東西真的會比較麻煩,而且在 Rust 中大量的使用了 RAII ,我可以不用擔心忘記釋放任何東西 (反倒要擔心東西不小心被釋放了,但「基本上」也不會發生) ,雖然剛開始被 lifetime 搞的很頭痛,但習慣後效率真的很高。

鐵人賽後記

之前就一直想參加鐵人賽了,但是要寫什麼好呢?一直拿不定主意,介紹前端的 React 與 Vue 已經一堆了,競爭感覺很激烈,雖然我對於自己對這兩個東西的熟悉程度有自信,包含對它們底層的實作多多少少都有了解,我不只對新的東西感興趣,還對它們背後怎麼實現的感興趣,所以我覺得開源程式真是個好東西,多虧了開源,我去翻了 Vue 的 observer 的實作,去翻了 React 怎麼處理 event ,去翻了 ptrace 怎麼 trace system call 之類的,稍微扯遠了,但我對寫好文章沒什麼自信,我沒什麼寫文章的習慣的,那就挑個冷門點的題目吧, Rust 這個語言也急速的在發展,或許以後會有不少人來用它寫些需要速度,或是偏底層的程式吧。

第一篇文章出來後,如果有編輯紀錄你應該會看到我反覆修了好幾次,那時貼出第一篇後拿給我朋友看,朋友:「我還以為你會先排版再貼上去」,於是我反覆修正了排版與用詞,雖然我從一開始就知道我的文章不適合初學者當教材了,我自己是覺得我的我文章像 Rust By Examples ,之前有位教授這樣說:「學生總是很困惑,老師在講什麼怎麼都聽不懂。老師也很困惑,學生到底為什麼不懂。」,一旦學會了什麼東西就會覺得很簡單,要再來教人就不容易了,雖然我也挺喜歡教人的,但有時我不知道自己的教法是不是正確的。

鐵人賽真的給了我個不錯的機會,有理由讓自己去寫文章,而且到後面還每天寫個專案(X ,其實除了網頁後端與資料庫那兩篇的專案都有事先準備外,其它的專案都是當天或前一天開始趕工做出來的,至於文章除了前幾篇外也都是當天新鮮的,所以那天如果到 10 點以後才發文大概是我那之前在除錯,這週花在鐵人賽的文章與相關的專案上的時數接近完整的 1 天啊,這次鐵人賽也讓我接觸到了兩樣之前沒碰過的,一個是 WebAssembly ,另一個是用 Rust 寫 GTK 程式 (之前只有用 C 寫過)。

最後一篇好像大多數人都會直接寫心得了,不過我還是有附上一點教學,因為理論上我的心得通常不會寫太多字,不過這個後記居然有將近 800 字其實我也是有點驚訝,要是我以前寫讀書心得也能寫的這麼順就好了。

總之謝謝正在看文章的你,以及之前提供我建議的讀者,拿 GTK 做井字棋可說完全是因為那則留言而產生的。

對了,原本我還想在這篇示範怎麼發佈個 crate ,然後把之前寫的那個 hastebin-client 發上去的,不過我還是先把程式碼整理一下吧。

變數的所有權與借出變數 Move, Borrow & Ownership

這篇與下一篇要介紹 Rust 中可說是最複雜,卻也是最重要的一個觀念,變數的所有權 (ownership) ,在 Rust 中每個變數都有其所屬的範圍 (scope) ,在變數的有效的範圍中,可以選擇將變數「借 (borrow)」給其它的 scope ,也可以將所有權整個轉移 (move) 出去,送給別人喔,當然,送出去的東西如果別人不還你的話是拿不回來的,但借出去的就只是暫時的給別人使用而已。

Move

fn main() {
    let message = String::from("Hello");
    {
        message;
    }
    println!("{}", message);
}
fn greet(message: String) {
    println!("{}", message);
}

fn main() {
    let message = String::from("Hello");
    greet(message);
    println!("{}", message);
}

猜看看上面的兩段程式碼的執行結果是什麼,猜到了嗎,答案都是無法編譯,編譯器會出現:

error[E0382]: use of moved value: `message`

意思是使用了已經送給別人的變數,在 Rust 中一個程式碼的區塊, 也就是由 {} 包圍的區域都是一個 scope ,這也包含了函式、迴圈的括號等等,只要你把變數傳給了其它區塊,都會把變數送出去,所以在上面的範例中, message 這個變數已經送出去,並且在接下來的 println! 無法使用了,另外在底下的情況也會送出變數:

#![allow(unused)]
fn main() {
let a = String::from("a");
let b = a;
println!("{}", a); // 這邊也同樣不能編譯
}

或許你已經注意到了,這邊使用的都是 String::from ,都是在建立字串,如果把上面的例子都換成數字的話,你會發現不會出現任何錯誤,而能順利的執行,因為數字可以 複製 ,字串不能複製嗎?也可以,只是字串的大小並不固定,有可能是很長的一篇文章,也有可能是一個空字串, Rust 並不允許在沒有明確的說要複製的情況下複製這種不知道會花費多少成本的型態,如果要改寫上面的範例,複製一個字串的話,可以使用 clone

#![allow(unused)]
fn main() {
let a = String::from("a");
let b = a.clone();
println!("{}", a);
}

數字的大小則是固定的,於是在發生把變數送出去的情況時, Rust 會使用複製一份的方式給別人,所以就變成了兩個人都擁有,不會發生錯誤的情況。

如果你想知道哪個型態可以被複製,可以參考文件的 std::marker::Copy ,你會在底下看到如 impl Copy for i32 這就代表 i32 可以被複製

拿走的東西主動的還回去也是可以的:

// 我要拿走整個 message 變數
fn greet(message: String) -> String {
    println!("{}", message);
    message // 之後再還回去
}

fn main() {
    let message = String::from("Hello");

    // 這邊變數被拿走了,但是又還了回來,於是我們需要一個變數代表它
    // 當然你也可以使用同樣的名稱 message
    let msg = greet(message);

    println!("{}", msg); // 又拿回來了,於是可以使用
}

Borrow

Rust 中把出借變數直接稱為 borrow , Rust 中使用在變數前面加一個 & 來代表出借變數,borrow 的用途是當你不想把變數送出去時,你就可以把你的變數 出去,但還有個前提是對方要 願意跟你借 ,底下是個借出變數給函式的範例:

// 這邊在 String 的前面加上了 & 代表我可以跟別人用借的
fn greet(message: &String) {
  println!("{}", message);
}

fn main() {
  let message = String::from("Hello");
  greet(&message); // 這邊加上了 & 來表示借出去
  println!("{}", message); // 借出去的東西只是暫時給別人而已,自己還可以使用
}
// 這邊沒有加上 & 代表我想要整個拿走
fn greet(message: String) {
  println!("{}", message);
}

fn main() {
  let message = String::from("Hello");
  // greet(&message); // 這邊就算加上了 & 也沒辦法把變數用借的借出去
  greet(message); // 一定要整個給它
  // println!("{}", message); // 因為被整個拿走了,所以這邊已經沒辦法使用了
}

Rust 預設借給別人的東西別人必須原封不動的還回來,也就是借出去的變數是沒辦法被修改的,如果你想允許別人修改的話,你就必須使用 &mut 對方也必須明確的使用 &mut 來代表我要借到一個可以修改的變數:

fn combine_string(target: &mut String, source: &String) {
  // push_str 會把傳進去的字串接到字串的後面
  target.push_str(source);
}

fn main() {
  // 這邊一定要加 mut ,因為這個變數會被修改,就算不是你自己改的也一樣
  let mut message = String::from("Hello, ");
  let world = String::from("World");
  // 借給 combine_string 一個可以改的變數 message ,與一個不能改的 world
  combine_string(&mut message, &world);
  println!("{}", message); // 這邊就會印出 Hello, World
}

還記得前一篇的猜數字裡有 stdin().read_line(&mut input) 嗎?

Borrow 的規則

Rust 的出借變數是有其規則在的:

  1. 所有的變數一次都只能用可以修改的方式 (&mut) 出借一次
#![allow(unused)]
fn main() {
let mut n = 42;
let a = &mut n;
let b = &mut n; // 這裡用可以修改的方式總共借出去兩次了,這是不可以的
}
  1. 可以無限的用唯讀的方式借出去
#![allow(unused)]
fn main() {
let n = 42;
let a = &n;
let b = &n;
}
  1. 一旦用可以修改的方式 (&mut) 出借,那你就不能用任何其它的方式存取變數了
#![allow(unused)]
fn main() {
let mut n = 42;
{
  let a = &mut n;
  // println!("{}", n); // 你不可以使用原本的 n
  // let b = &n; // 你也不可以再用任何方式借走 n
}
println!("{}", n); // 我們離開了 a 借走 n 的範圍了,於是 n 又可以用了
}
  1. 一旦你用唯讀的方式借出了變數,你就不可以修改變數
#![allow(unused)]
fn main() {
let mut n = 42;
{
  let a = &n;
  // n = 123; // 又不可以了,有夠煩的(X
}
n = 123; // 這邊才可以修改
}

這些規則是用來確保多執行緒時不會有資料競爭用的,也就是同時有兩個人修改了同一個變數,於是一次只允許有一個變數的擁有者能修改變數的值,同時一但借出了變數就不能隨意修改,因為別人不一定會知道變數被修改了。雖然有點麻煩 (也真的很麻煩) ,但往好處想,變數不再會被隨意的修改了。

有點可惜的是目前的 borrow checker ,也就是檢查,並執行上面這些規則的功能,它並不是很完善,比如:

#![allow(unused)]
fn main() {
let mut array = [123, 456];
let a = &mut array[0];
let b = &mut array[1];
}

兩個變數分別借走了不相干的兩個部份,但這沒辦法通過檢查,不過這在 Rust 2018 將會有所改善,敬請期待。

Q: Rust 2018 是什麼? A: 在今年的年底 Rust 將要推出 2018 年版,版本號會是 1.30 ,將會有不少的改進以及部份的語法的變更。 Q: 什麼!那我現在學的這些東西到年底就都沒辦法用了? A: 放心好了,大部份的是功能的增強與新的語法,只有一小部份的修改,之後會有一篇來討論這些修改,與看看有哪些新功能。 Q: 那我不想更新可以嗎? A: 可以,你可以設定使用現在的語法版本,也就是 Rust 2015 版。 Q: 那我要怎麼設定? A: 這個之後再說。

String & str, Array & Slice

我們之前應該有提到過 Rust 有兩種字串 Stringstr ,可是一直沒有詳細說明這兩個的差別,這邊我們要提到 Rust 的一個東西「切片 (slice)」,切片可以理解為一次出借如陣列或字串這類的連續的資料型態的一部份:

如果你有寫過 Python 你可能知道 Python 的切片 array[1:3] ,只是這邊把 : 換成了 .. 而已。

#![allow(unused)]
fn main() {
let mut array = [0, 1, 2, 3, 4, 5];
{ // 建立一個區塊,不然我們等下沒辦法使用原本的 array
  let slice: &mut [i32] = &mut array[1..3]; // 這邊一次的借走了 array 的第 2 跟第 3 個元素

  // 然後我們修改了切片的第 1 個元素,對應到原本的 array 則是第二個元素
  slice[0] = 42;

  println!("{:?}", slice); // 會印出 [42, 2]
}
println!("{:?}", array); // 印出 [0, 42, 2, 3, 4, 5]
}

Rust 的切片會知道自己借走了多少長度的東西,而且跟原本的變數 會共用同一塊空間 ,建立切片是不會複製任何資料的。

你可以看到這邊的印出來的結果很明顯的修改了原本的資料,同時很重要的一點,切片 只能有 borrow 的型態 ,因為切片的本質就是出借資料,切片能把資料出借一小段,而使用者可以把這段資料當成像陣列一樣使用。

{:?} 是把資料以 debug 的方式印出來,內建的型態不一定能直接印出來,但大部份都能用這種方式印出來,如果不能使用 {} 印出來時 {:?} 通常能派上用場。

上面的 slice 的型態是 &mut [i32] ,這就是切片型態的寫法,一般如果需要借走一個陣列都會使用切片型態,這樣可以給予使用者更大的彈性,比如決定要不要把整個陣列都借出去,或是隻借出一部份。

那終於可以來講 str 了, str 事實上就是字串的切片,而 String 則是一個可以在執行時改變大小的字串:

#![allow(unused)]
fn main() {
// 直接使用雙引號 (") 的字串都是字串的切片,它們都被 Rust 放在某個地方並且借給使用者使用而已
let hello: &str = "Hello";
// 建立一個 String
let string: String = String::from(hello);
// 借走字串的一部份,產生一個字串切片
let part_of_string: &str = &string[1..3];
}

同樣的 str 也只能有 borrow 的型態。

下一篇要來介紹一下 borrow 的存活時間 (lifetime) 同樣也是重要觀念,這兩篇都是在講觀念可能比較無聊,不過接下來我們就會繼續介紹程式的語法了。

Lifetime: Borrow 的存活時間

Rust 有個重要的功能叫 borrow checker ,它除了檢查在上一篇提到的規則外,還檢查使用者會不會使用到懸空參照 (dangling reference) ,懸空參照是在電腦世界中一種現象: 如果你今天把一個變數借給別人,實際上借走的人只是知道我可以去哪裡找到這個別人借我的東西而已,那個東西的擁有者還是你本人,以現實世界做比喻的話,這像是借別人東西只是把放那個東西的儲物櫃位置,以及鑰匙暫時的交給別人而已,送別人東西則是直接把儲物櫃的擁有者變成他。

所以如果今天發生了一種情況,你把東西借給別人後,管理每個儲物櫃擁有者的系統馬上把你的使用權收回去呢?會發生什麼事,這沒人說的準,可能儲物櫃還沒被清空,你還是可以拿到借來的東西,或是馬上又換了主人,你已經不是拿到原本的東西了,就像以下的程式碼:

#![allow(unused)]
fn main() {
fn foo() -> &i32 {
    // 這個變數在離開這個範圍後就消失了
    let a = 42;
    // 但是這邊卻回傳了 borrow
    &a
}
}

上面這段 code 是無法編譯的。

為瞭解決這樣的一個問題, Rust 提出來的就是 lifetime 的觀念,只要函式的參數或回傳值有 borrow 出現,使用者就要幫 borrow 標上 lifetime ,標記後讓編譯器可以去追蹤每個變數借出去與釋放掉的情況,確保不會有釋放掉已經出借的變數的可能性。

Rust 使用 'a 一個單引號加上一個識別字當作 lifetime 的標記,所以這些都是可以的 'b, 'foo, '_bar ,此外有兩個保留用作特殊用途的 lifetime: 'static'_

  • 'static: 這代表這是個整個程式都有效的 borrow 比如字串常數 "foo" 它的 lifetime 就是 'static
  • '_:這是保留給 Rust 2018 使用的,這裡先不提它的功能

這邊是個加上 lifetime 標記後的範例:

#![allow(unused)]
fn main() {
fn foo<'a>(a: &'a i32) -> &'a i32 {
    a
}
}

其中我們必須在函式名稱後加上 <> 並在其中宣告我們的 lifetime ,接著把 borrow 的 & 後都加上我們的 lifetime 標記,但事實上在上一篇文章中,我們完全沒用使用到 lifetime , Rust 可以在某些情況下自動推導出正確的 lifetime ,使得實際上需要手動標註的情況並不多,最有可能遇到的情況是一個函式同時使用了兩個 borrow :

fn max<'a>(a: &'a i32, b: &'a i32) -> &'a i32 {
    if a > b {
        a
    } else {
        b
    }
}

fn main() {
    let a = 3;
    let m = &a;
    {
        let b = 2;
        let n = &b;
        // 對於 max 來說, m 與 n 同時存活的這個範圍就是 'a ,
        // 而回傳值也可以在這個範圍內使用
        println!("{}", max(m, n));
    } // b 與 n 會在這邊消失
} // a 與 m 會在這邊消失

這種情況編譯器因為看到了兩個 borrow ,於是沒辦法猜出來回傳的值應該要跟哪個 lifetime 一樣,這邊的作法就是全部都標記一樣的 lifetime ,讓 Rust 知道說我們的變數都會存活在同一個範圍內,同時回傳值也可以在同樣的範圍存活。

大部份的情況下編譯器都能自動的推導,所以需要手動標註的情況其實不多,通常是先嘗試讓編譯器做推導,如果編譯器報錯了才來想辦法標註。

lifetime 還有個用途是用來限制使用者傳入的參數必須是常數:

#![allow(unused)]
fn main() {
fn print_message(message: &'static str) {
  println!("{}", message);
}
}

這個函式就只能接受如 "Hello" 這樣的常數了,雖說只是偶爾會有這樣的需求。

Lifetime Elision (Lifetime 省略規則) (進階)

這部份大概的瞭解一下就好了

  1. 所有的 borrow 都會自動的分配一個 lifetime
#![allow(unused)]
fn main() {
fn foo(a: &i32, b: &i32);
fn foo<'a, 'b>(a: &'a i32, b: &'b i32); // 推導結果
}
  1. 如果函式只有一個 borrow 的參數,則它的 lifetime 會自動被應用到回傳值上
#![allow(unused)]
fn main() {
fn foo(a: &i32);
fn foo<'a>(a: &'a i32) -> &'a i32; // 推導結果
}
  1. 如果有多個 borrow ,但其中一個是 self ,則 self 的 lifetime 會被應用在回傳值
#![allow(unused)]
fn main() {
impl Foo {
    fn method(&self, a: &i32) -> &Self {}
}

// 推導結果
impl Foo {
    fn method<'a, 'b>(&'a self, b: &'b i32) -> &'a Self {}
}
}

若不符合上面任一條規則,則必須要標註型態。

如果我們把以上的規則套用在上面的範例 max 上:

#![allow(unused)]
fn main() {
fn max(a: &i32, b: &i32) -> &i32 {
    if a > b {
        a
    } else {
        b
    }
}
}

套用規則 1 :

#![allow(unused)]
fn main() {
fn max<'a, 'b>(a: &'a, i32, b: &'b i32) -> &i32 {
    if a > b {
        a
    } else {
        b
    }
}
}

到這邊結束,編譯器已經沒有可用的規則了,但是回傳值的 lifetime 依然是未知,於是就編譯失敗。

C++ vs. Rust: 全面特性比較

本文檔旨在全面比較 C++ 和 Rust 這兩種高效能系統程式語言的關鍵特性。C++ 以其悠久的歷史、強大的生態系統和對硬體的精細控制而聞名,而 Rust 則以其創新的所有權系統、對記憶體安全和併發安全的編譯期保證而備受關注。

每個範例都提供可直接編譯和執行的程式碼。

1. 記憶體管理

這是兩種語言最根本的區別。C++ 依賴手動管理和智慧指標,而 Rust 引入了所有權系統。

C++: RAII 與智慧指標

C++ 透過 RAII (Resource Acquisition Is Initialization) 模式管理資源。現代 C++ 強烈推薦使用智慧指標 (std::unique_ptr, std::shared_ptr) 來自動化記憶體管理,避免手動 newdelete

範例 (main.cpp):

#include <iostream>
#include <memory>
#include <string>

class Entity {
private:
    std::string name;
public:
    Entity(const std::string& name) : name(name) {
        std::cout << "Entity '" << name << "' created." << std::endl;
    }
    ~Entity() {
        std::cout << "Entity '" << name << "' destroyed." << std::endl;
    }
    void greet() {
        std::cout << "Hello, I am " << name << "." << std::endl;
    }
};

int main() {
    // 使用 unique_ptr,當 ptr 離開作用域時,記憶體會被自動釋放
    auto ptr = std::make_unique<Entity>("Player1");
    ptr->greet();
    
    // 不需要手動 delete ptr;
    return 0;
}

編譯與執行:

g++ main.cpp -o main_cpp -std=c++17
./main_cpp

Rust: 所有權、借用與生命週期

Rust 的核心是其所有權系統,它在編譯期強制執行記憶體安全規則:

  1. 每個值都有一個擁有者(owner)。
  2. 同一時間只能有一個擁有者。
  3. 當擁有者離開作用域時,值會被丟棄(dropped)。

可以透過「借用」(borrowing)來臨時參考一個值,而不會轉移所有權。

範例 (main.rs):

struct Entity {
    name: String,
}

impl Entity {
    fn new(name: &str) -> Self {
        println!("Entity '{}' created.", name);
        Self { name: name.to_string() }
    }

    fn greet(&self) {
        println!("Hello, I am {}.", self.name);
    }
}

impl Drop for Entity {
    fn drop(&mut self) {
        println!("Entity '{}' destroyed.", self.name);
    }
}

fn main() {
    // e 擁有 Entity 的所有權
    let e = Entity::new("Player1");
    e.greet();
    
    // 當 main 函式結束時,e 離開作用域,其擁有的資源會被自動釋放
}

編譯與執行:

rustc main.rs -o main_rs
./main_rs

2. 資料型別 (Structs & Enums)

C++: Structs 與 Enum Classes

C++ 的 struct 用於組合資料。enum class 是現代 C++ 中推薦的列舉類型,因為它提供了型別安全。

範例 (main.cpp):

#include <iostream>

struct Point {
    double x;
    double y;
};

enum class Color {
    Red,
    Green,
    Blue
};

void print_color(Color c) {
    switch(c) {
        case Color::Red:
            std::cout << "Color is Red" << std::endl;
            break;
        case Color::Green:
            std::cout << "Color is Green" << std::endl;
            break;
        case Color::Blue:
            std::cout << "Color is Blue" << std::endl;
            break;
    }
}

int main() {
    Point p = {10.5, 20.3};
    std::cout << "Point: (" << p.x << ", " << p.y << ")" << std::endl;
    
    Color c = Color::Green;
    print_color(c);
    
    return 0;
}

編譯與執行:

g++ main.cpp -o main_cpp -std=c++17
./main_cpp

Rust: Structs 與強大的 Enums (代數資料型別)

Rust 的 struct 與 C++ 類似。然而,Rust 的 enum 是功能強大的代數資料型別(Sum Types),每個變體都可以攜帶不同型別和數量的資料。match 控制流程運算式是處理 enum 的理想方式。

範例 (main.rs):

struct Point {
    x: f64,
    y: f64,
}

// Rust 的 enum 可以包含資料
enum Shape {
    Circle(Point, f64), // 中心點和半徑
    Rectangle(Point, Point), // 左上角和右下角點
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(_, radius) => std::f64::consts::PI * radius * radius,
            Shape::Rectangle(p1, p2) => ((p2.x - p1.x) * (p2.y - p1.y)).abs(),
        }
    }
}

fn main() {
    let p = Point { x: 0.0, y: 0.0 };
    println!("Point: ({}, {})", p.x, p.y);

    let circle = Shape::Circle(Point { x: 0.0, y: 0.0 }, 10.0);
    let rect = Shape::Rectangle(Point { x: 0.0, y: 0.0 }, Point { x: 10.0, y: 20.0 });

    println!("Circle area: {}", circle.area());
    println!("Rectangle area: {}", rect.area());
}

編譯與執行:

rustc main.rs -o main_rs
./main_rs

3. 錯誤處理

C++: 例外 (Exceptions)

C++ 的主要錯誤處理機制是例外。當錯誤發生時,可以 throw 一個例外,並在 try...catch 區塊中捕獲它。

範例 (main.cpp):

#include <iostream>
#include <stdexcept>

double divide(double a, double b) {
    if (b == 0.0) {
        throw std::runtime_error("Division by zero!");
    }
    return a / b;
}

int main() {
    try {
        double result = divide(10.0, 0.0);
        std::cout << "Result: " << result << std::endl;
    } catch (const std::runtime_error& e) {
        std::cerr << "Error caught: " << e.what() << std::endl;
    }
    return 0;
}

編譯與執行:

g++ main.cpp -o main_cpp -std=c++17
./main_cpp

Rust: ResultOption Enums

Rust 沒有例外。它使用 Result<T, E>Option<T> 這兩個 enum 來處理可恢復和不可恢復的錯誤。Result 用於可能失敗的操作,Option 用於可能為空的值。這使得錯誤處理在型別系統中是明確的。

範例 (main.rs):

// 函式返回一個 Result,Ok 包含成功的值,Err 包含錯誤資訊
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Division by zero!".to_string())
    } else {
        Ok(a / b)
    }
}

fn main() {
    match divide(10.0, 0.0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => eprintln!("Error caught: {}", e),
    }
    
    match divide(10.0, 2.0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => eprintln!("Error caught: {}", e),
    }
}

編譯與執行:

rustc main.rs -o main_rs
./main_rs

4. 併發 (Concurrency)

C++: std::thread 與 Mutexes

C++11 引入了標準的執行緒支援。開發者需要手動使用 std::mutex 等同步原語來防止資料競爭(Data Races)。

範例 (main.cpp):

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>

std::mutex mtx;
int counter = 0;

void increment() {
    for (int i = 0; i < 10000; ++i) {
        std::lock_guard<std::mutex> lock(mtx); // RAII-style lock
        counter++;
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.push_back(std::thread(increment));
    }

    for (auto& th : threads) {
        th.join();
    }

    std::cout << "Final counter: " << counter << std::endl;
    return 0;
}

編譯與執行:

g++ main.cpp -o main_cpp -std=c++17 -pthread
./main_cpp

Rust: 安全的併發

Rust 的所有權和借用規則在編譯期就能防止資料競爭。跨執行緒共享資料需要使用 Arc<Mutex<T>> (原子引用計數的互斥鎖),Rust 編譯器會確保你在存取資料前正確地鎖定了互斥鎖。

範例 (main.rs):

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Arc 用於多所有權,Mutex 用於互斥存取
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter_clone.lock().unwrap(); // 鎖定 Mutex
            for _ in 0..10000 {
                *num += 1;
            }
        }); // Mutex 在這裡自動解鎖
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Final counter: {}", *counter.lock().unwrap());
}

編譯與執行:

rustc main.rs -o main_rs
./main_rs

5. 泛型 (Generics)

C++: 樣板 (Templates)

C++ 使用樣板來實現泛型程式設計。樣板在編譯期進行實體化,非常強大靈活,但錯誤訊息可能很冗長。C++20 引入了 Concepts 來約束樣板參數。

範例 (main.cpp):

#include <iostream>

template<typename T>
void print_value(T value) {
    std::cout << "Value: " << value << std::endl;
}

int main() {
    print_value(42);
    print_value(3.14);
    print_value("Hello C++");
    return 0;
}

編譯與執行:

g++ main.cpp -o main_cpp -std=c++17
./main_cpp

Rust: 泛型與 Trait 約束

Rust 的泛型透過 trait 來約束。trait 類似於介面,定義了泛型型別必須實現的行為。這使得泛型程式碼更安全,錯誤訊息也更清晰。

範例 (main.rs):

use std::fmt::Display;

// T 必須實現 Display trait,這樣才能被格式化輸出
fn print_value<T: Display>(value: T) {
    println!("Value: {}", value);
}

fn main() {
    print_value(42);
    print_value(3.14);
    print_value("Hello Rust");
}

編譯與執行:

rustc main.rs -o main_rs
./main_rs

6. 可變性 (Mutability)

C++: 預設可變

在 C++ 中,變數預設是可變的。const 關鍵字用於宣告不可變的變數、指標或方法。

範例 (main.cpp):

#include <iostream>

int main() {
    int mutable_var = 10;
    mutable_var = 20; // OK

    const int immutable_var = 30;
    // immutable_var = 40; // 編譯錯誤
    
    std::cout << "mutable_var: " << mutable_var << std::endl;
    std::cout << "immutable_var: " << immutable_var << std::endl;
    return 0;
}

編譯與執行:

g++ main.cpp -o main_cpp -std=c++17
./main_cpp

Rust: 預設不可變

在 Rust 中,變數預設是不可變的。必須使用 mut 關鍵字來明確宣告一個變數是可變的。這有助於編寫更安全、更易於推理的程式碼。

範例 (main.rs):

fn main() {
    let immutable_var = 10;
    // immutable_var = 20; // 編譯錯誤

    let mut mutable_var = 30;
    mutable_var = 40; // OK

    println!("immutable_var: {}", immutable_var);
    println!("mutable_var: {}", mutable_var);
}

編譯與執行:

rustc main.rs -o main_rs
./main_rs

7. 巨集 (Macros)

C++: 前置處理器巨集

C++ 的巨集由前置處理器處理,基本上是文字替換。功能強大但缺乏型別安全,且容易產生意想不到的副作用。

範例 (main.cpp):

#include <iostream>

#define SQUARE(x) ((x) * (x))

int main() {
    int a = 5;
    std::cout << "Square of 5 is " << SQUARE(a) << std::endl;
    // 容易出錯的例子: SQUARE(2 + 3) -> ((2 + 3) * (2 + 3)) -> 25 (正確)
    // 如果定義為 #define SQUARE(x) x * x, 則 SQUARE(2+3) -> 2+3*2+3 -> 11 (錯誤)
    return 0;
}

編譯與執行:

g++ main.cpp -o main_cpp -std=c++17
./main_cpp

Rust: 程序化與宣告式巨集

Rust 的巨集系統更先進,是語法層面的抽象,直接操作 AST(抽象語法樹)。它們是衛生的(Hygienic),避免了變數名稱衝突,並且型別安全。

範例 (main.rs):

// 宣告式巨集
macro_rules! create_function {
    ($func_name:ident, $output:expr) => {
        fn $func_name() {
            println!("{}", $output);
        }
    };
}

// 使用巨集來建立一個函式
create_function!(say_hello, "Hello from a macro!");

fn main() {
    // 呼叫由巨集產生的函式
    say_hello();
}

編譯與執行:

rustc main.rs -o main_rs
./main_rs

8. 建置系統與套件管理

C++: 破碎的生態系

C++ 沒有官方統一的建置系統或套件管理器。CMake 是事實上的標準建置系統,但學習曲線陡峭。套件管理通常依賴 Conanvcpkg 或手動管理。

典型流程 (CMake):

  1. 撰寫 CMakeLists.txt
  2. mkdir build && cd build
  3. cmake ..
  4. make

Rust: Cargo - 整合式工具鏈

Rust 內建了 Cargo,一個極其強大的建置系統和套件管理器。它處理:

  • 專案建立 (cargo new)
  • 建置 (cargo build)
  • 執行 (cargo run)
  • 測試 (cargo test)
  • 文件產生 (cargo doc)
  • 依賴管理 (透過 Cargo.toml)
  • 發布到 crates.io (cargo publish)

典型流程 (Cargo):

  1. cargo new my_project
  2. cd my_project
  3. (在 Cargo.toml 中加入依賴)
  4. cargo run

結論

特性C++Rust
核心哲學你不為你不使用的東西付費,信任程式設計師安全性、併發性、效能,不信任程式設計師
記憶體管理手動、RAII、智慧指標所有權、借用、生命週期 (編譯期保證)
安全性容易出現未定義行為 (懸垂指標、緩衝區溢位)記憶體安全和執行緒安全 (編譯期保證)
錯誤處理例外 (Exceptions)ResultOption Enums
併發手動鎖定,容易出錯無資料競爭的併發 (編譯期保證)
可變性預設可變預設不可變
工具鏈破碎 (CMake, Make, Conan, vcpkg...)統一且強大 (Cargo)
學習曲線極其陡峭,充滿陷阱陡峭,但主要是所有權系統,一旦掌握就相對平滑

選擇建議:

  • 選擇 C++:當你需要與龐大的現有 C++ 程式碼庫整合、利用成熟的 C++ 函式庫生態、或需要進行極低階的硬體操作時。
  • 選擇 Rust:當你開始一個新專案,且對系統的可靠性、記憶體安全和併發安全有極高要求時。Rust 的現代化工具鏈和強大的編譯期保證可以顯著提高開發效率和軟體品質。

// C++ Move 語義 vs Rust 所有權系統 深度對比 // =============================================

/* 核心差異總結:

C++ Move:

  • 運行時優化機制
  • 可選的,編譯器決定
  • 移動後對象仍存在但處於"已移動"狀態
  • 可能意外訪問已移動對象
  • 需要程序員理解和正確使用

Rust Ownership:

  • 編譯時強制的內存安全機制
  • 必須的,編譯器強制執行
  • 移動後原變量不可再訪問
  • 編譯器防止任何誤用
  • 自動處理,零運行時開銷 */

// ============================================ // 1. 基本概念對比 // ============================================

/* C++ Move 語義示例:

#include #include #include #include

class Resource { private: std::vector data; std::string name;

public: // 構造函數 Resource(const std::string& n) : name(n) { data.resize(1000, 42); std::cout << "資源 " << name << " 創建\n"; }

// 複製構造函數
Resource(const Resource& other) : data(other.data), name(other.name + "_copy") {
    std::cout << "資源 " << name << " 被複製\n";
}

// 移動構造函數
Resource(Resource&& other) noexcept 
    : data(std::move(other.data)), name(std::move(other.name)) {
    std::cout << "資源 " << name << " 被移動\n";
    // other 仍然存在,但資源已被"偷走"
}

// 複製賦值
Resource& operator=(const Resource& other) {
    if (this != &other) {
        data = other.data;
        name = other.name + "_assigned";
        std::cout << "資源 " << name << " 被賦值複製\n";
    }
    return *this;
}

// 移動賦值
Resource& operator=(Resource&& other) noexcept {
    if (this != &other) {
        data = std::move(other.data);
        name = std::move(other.name);
        std::cout << "資源 " << name << " 被賦值移動\n";
    }
    return *this;
}

~Resource() {
    std::cout << "資源 " << name << " 被銷毀\n";
}

void use_resource() {
    std::cout << "使用資源 " << name << " (大小: " << data.size() << ")\n";
}

};

void cpp_move_examples() { std::cout << "=== C++ Move 語義 ===\n";

// 1. 基本移動
Resource r1("original");
Resource r2 = std::move(r1);  // 移動構造

// 危險:r1 仍然存在但已被移動,訪問可能導致未定義行為
// r1.use_resource();  // 可能崩潰或產生錯誤結果

r2.use_resource();  // 安全

// 2. 函數參數移動
auto create_resource = []() -> Resource {
    return Resource("temp");  // 自動移動(RVO可能跳過)
};

Resource r3 = create_resource();  // 移動或複製

// 3. 容器中的移動
std::vector<Resource> resources;
resources.push_back(std::move(r2));  // 移動到容器中
// r2 現在處於已移動狀態

// 4. 錯誤:意外使用已移動對象
// r2.use_resource();  // 未定義行為!

}

int main() { cpp_move_examples(); return 0; }

*/

// Rust 所有權系統 - 相同功能的安全實現 use std::fmt;

#[derive(Debug)] struct Resource { data: Vec, name: String, }

impl Resource { fn new(name: &str) -> Self { println!("資源 {} 創建", name); Resource { data: vec![42; 1000], name: name.to_string(), } }

fn use_resource(&self) {
    println!("使用資源 {} (大小: {})", self.name, self.data.len());
}

fn use_resource_mut(&mut self) {
    self.data.push(99);
    println!("修改資源 {} (新大小: {})", self.name, self.data.len());
}

}

impl Drop for Resource { fn drop(&mut self) { println!("資源 {} 被銷毀", self.name); } }

impl Clone for Resource { fn clone(&self) -> Self { println!("資源 {} 被克隆", self.name); Resource { data: self.data.clone(), name: format!("{}_clone", self.name), } } }

fn rust_ownership_examples() { println!("=== Rust 所有權系統 ===");

// 1. 基本移動(所有權轉移)
let r1 = Resource::new("original");
let r2 = r1;  // 所有權移動,r1 不再可用

// 編譯錯誤:r1 已被移動
// r1.use_resource();  // 編譯錯誤!

r2.use_resource();  // 安全

// 2. 函數參數所有權轉移
fn take_ownership(resource: Resource) {
    resource.use_resource();
    // resource 在函數結束時被銷毀
}

fn borrow_resource(resource: &Resource) {
    resource.use_resource();
    // 只是借用,不獲取所有權
}

fn borrow_mut_resource(resource: &mut Resource) {
    resource.use_resource_mut();
    // 可變借用,可以修改但不獲取所有權
}

let r3 = Resource::new("for_function");

// 選項1:轉移所有權
// take_ownership(r3);  // r3 被移動,不再可用

// 選項2:借用(推薦)
borrow_resource(&r3);  // r3 仍然可用

// 選項3:可變借用
let mut r4 = Resource::new("mutable");
borrow_mut_resource(&mut r4);
r4.use_resource();  // 仍然可用

// 3. 容器中的所有權
let mut resources = Vec::new();
let r5 = Resource::new("for_vector");
resources.push(r5);  // r5 被移動到 vector 中
// r5 不再可用

// 訪問 vector 中的資源
if let Some(resource) = resources.get(0) {
    resource.use_resource();
}

// 4. 所有權恢復
let recovered = resources.pop().unwrap();  // 從 vector 中取回所有權
recovered.use_resource();

}

// ============================================ // 2. 深度對比:複雜場景 // ============================================

/* C++ 複雜移動場景的問題:

class ComplexResource { std::unique_ptr<int[]> buffer; size_t size; std::string id;

public: ComplexResource(const std::string& identifier, size_t s) : buffer(std::make_unique<int[]>(s)), size(s), id(identifier) {}

// 移動構造後,原對象狀態不一致
ComplexResource(ComplexResource&& other) noexcept
    : buffer(std::move(other.buffer)), size(other.size), id(std::move(other.id)) {
    // 問題:other.size 仍然保留原值,但 other.buffer 為 null
    // 這可能導致不一致狀態
}

void process() {
    if (buffer) {  // 必須檢查,因為可能已被移動
        for (size_t i = 0; i < size; ++i) {
            buffer[i] = i;
        }
    } else {
        // 已被移動的對象被意外使用
        std::cout << "錯誤:嘗試使用已移動的資源\n";
    }
}

size_t get_size() const { return size; }  // 可能返回錯誤信息

};

void problematic_move_usage() { ComplexResource cr1("test", 100); ComplexResource cr2 = std::move(cr1);

// 危險:cr1 處於部分移動狀態
std::cout << "cr1 size: " << cr1.get_size() << "\n";  // 仍然返回 100
cr1.process();  // 但實際上 buffer 是 null!

}

*/

// Rust 等價實現 - 編譯時安全 struct ComplexResource { buffer: Vec, // 自動管理內存 id: String, }

impl ComplexResource { fn new(identifier: &str, size: usize) -> Self { ComplexResource { buffer: vec![0; size], id: identifier.to_string(), } }

fn process(&mut self) {
    for (i, item) in self.buffer.iter_mut().enumerate() {
        *item = i as i32;
    }
    println!("處理完成:{} 個元素", self.buffer.len());
}

fn get_size(&self) -> usize {
    self.buffer.len()
}

fn get_id(&self) -> &str {
    &self.id
}

}

fn safe_ownership_usage() { let mut cr1 = ComplexResource::new("test", 100); let cr2 = cr1; // 完整移動,cr1 完全不可用

// 編譯錯誤:無法訪問已移動的變量
// println!("cr1 size: {}", cr1.get_size());  // 編譯錯誤!
// cr1.process();  // 編譯錯誤!

println!("cr2 size: {}", cr2.get_size());  // 安全
// cr2.process();  // 需要可變引用

}

// ============================================ // 3. 性能對比 // ============================================

fn performance_comparison() { use std::time::Instant;

println!("\n=== 性能對比 ===");

// 測試大量所有權轉移
let start = Instant::now();

let mut resources = Vec::new();
for i in 0..10000 {
    let resource = Resource::new(&format!("resource_{}", i));
    resources.push(resource);  // 移動,零成本
}

// 處理所有資源
for resource in &resources {
    resource.use_resource();
}

let duration = start.elapsed();
println!("Rust 所有權轉移耗時: {:?}", duration);

// 測試借用性能
let start = Instant::now();

fn process_by_reference(resources: &[Resource]) {
    for resource in resources {
        resource.use_resource();
    }
}

process_by_reference(&resources);

let duration = start.elapsed();
println!("Rust 借用處理耗時: {:?}", duration);

}

// ============================================ // 4. 錯誤處理和安全性 // ============================================

/* C++ 移動相關的常見錯誤:

void common_cpp_move_errors() { // 錯誤1:使用已移動對象 std::vector v1 = {1, 2, 3, 4, 5}; std::vector v2 = std::move(v1); std::cout << v1.size() << std::endl; // 可能是0,但不保證

// 錯誤2:移動後再次移動
std::string s1 = "hello";
std::string s2 = std::move(s1);
std::string s3 = std::move(s1);  // 移動空字符串

// 錯誤3:在條件語句中移動
std::vector<int> data = {1, 2, 3};
if (some_condition) {
    process(std::move(data));
}
// 如果條件為假,data 仍然可用,但如果為真則不可用
data.push_back(4);  // 可能錯誤

// 錯誤4:返回本地變量的移動
auto create_vector = []() {
    std::vector<int> local = {1, 2, 3};
    return std::move(local);  // 不必要,反而可能阻止RVO
};

}

*/

// Rust 編譯時防止這些錯誤 fn rust_compile_time_safety() { println!("\n=== Rust 編譯時安全檢查 ===");

// 1. 防止使用已移動變量
let v1 = vec![1, 2, 3, 4, 5];
let v2 = v1;  // v1 被移動
// println!("{}", v1.len());  // 編譯錯誤!
println!("v2 長度: {}", v2.len());  // 安全

// 2. 防止重複移動
let s1 = String::from("hello");
let s2 = s1;  // s1 被移動
// let s3 = s1;  // 編譯錯誤!
println!("s2: {}", s2);

// 3. 條件移動的安全處理
let data = vec![1, 2, 3];
let some_condition = true;

let processed_data = if some_condition {
    // 移動到函數中處理
    process_data(data)
} else {
    // 如果不處理,保持原樣
    data
};

// processed_data 總是可用的
println!("處理後數據長度: {}", processed_data.len());

// 4. 返回值優化是自動的
fn create_vector() -> Vec<i32> {
    let local = vec![1, 2, 3];
    local  // 自動移動,無需顯式 move
}

let result = create_vector();
println!("創建的向量: {:?}", result);

}

fn process_data(mut data: Vec) -> Vec { data.push(4); data }

// ============================================ // 5. 高級所有權模式 // ============================================

// 智能指針和共享所有權 use std::rc::Rc; use std::cell::RefCell; use std::sync::{Arc, Mutex};

fn advanced_ownership_patterns() { println!("\n=== 高級所有權模式 ===");

// 1. 引用計數共享所有權 (單線程)
{
    let shared_data = Rc::new(vec![1, 2, 3, 4, 5]);
    let reference1 = Rc::clone(&shared_data);
    let reference2 = Rc::clone(&shared_data);
    
    println!("引用計數: {}", Rc::strong_count(&shared_data));
    println!("共享數據: {:?}", shared_data);
    
    // 所有引用離開作用域時,數據自動釋放
}

// 2. 內部可變性
{
    let data = Rc::new(RefCell::new(vec![1, 2, 3]));
    let ref1 = Rc::clone(&data);
    let ref2 = Rc::clone(&data);
    
    // 通過任何引用都可以修改
    ref1.borrow_mut().push(4);
    ref2.borrow_mut().push(5);
    
    println!("修改後的數據: {:?}", data.borrow());
}

// 3. 線程安全的共享所有權
{
    let shared_counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    
    for _ in 0..3 {
        let counter = Arc::clone(&shared_counter);
        let handle = std::thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("最終計數: {}", *shared_counter.lock().unwrap());
}

}

// ============================================ // 6. 生命週期和借用 // ============================================

// 生命週期參數確保引用有效性 fn lifetime_examples() { println!("\n=== 生命週期示例 ===");

// 1. 基本生命週期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

let string1 = String::from("long string");
let string2 = "short";
let result = longest(&string1, string2);
println!("最長的字符串: {}", result);

// 2. 結構體中的生命週期
#[derive(Debug)]
struct ImportantExcerpt<'a> {
    part: &'a str,
}

let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let excerpt = ImportantExcerpt {
    part: first_sentence,
};
println!("摘錄: {:?}", excerpt);

// 3. 複雜借用場景
fn demonstrate_borrowing() {
    let mut data = vec![1, 2, 3, 4, 5];
    
    // 不可變借用
    let sum: i32 = data.iter().sum();
    println!("總和: {}", sum);
    
    // 可變借用
    data.push(6);
    println!("添加元素後: {:?}", data);
    
    // 同時存在多個不可變借用
    let first = &data[0];
    let last = &data[data.len() - 1];
    println!("第一個: {}, 最後一個: {}", first, last);
    
    // 但不能同時存在可變和不可變借用
    // let mut_ref = &mut data;  // 編譯錯誤!
}

demonstrate_borrowing();

}

// ============================================ // 主函數和測試 // ============================================

fn main() { println!("🔄 C++ Move vs Rust Ownership 深度對比"); println!("======================================");

rust_ownership_examples();
safe_ownership_usage();
performance_comparison();
rust_compile_time_safety();
advanced_ownership_patterns();
lifetime_examples();

println!("\n📊 總結對比:");
println!("=============");

println!("C++ Move語義:");
println!("  ✅ 性能優化,減少不必要的複製");
println!("  ❌ 運行時概念,可能誤用已移動對象");
println!("  ❌ 需要程序員手動管理移動語義");
println!("  ❌ 移動後對象狀態可能不一致");

println!("\nRust 所有權系統:");
println!("  ✅ 編譯時保證內存安全");
println!("  ✅ 零運行時開銷");
println!("  ✅ 自動管理,無需手動干預");
println!("  ✅ 移動後原變量完全不可訪問");
println!("  ✅ 借用檢查器防止數據競爭");

println!("\n🎯 關鍵區別:");
println!("  • C++ Move: 優化機制,可選使用");
println!("  • Rust Ownership: 安全機制,強制執行");
println!("  • C++ 側重性能,Rust 側重安全");
println!("  • C++ 運行時檢查,Rust 編譯時檢查");

}

#[cfg(test)] mod tests { use super::*;

#[test]
fn test_ownership_transfer() {
    let resource = Resource::new("test");
    let moved_resource = resource;
    
    // 確保移動後可以正常使用
    moved_resource.use_resource();
    
    // 以下會編譯失敗,證明所有權已轉移
    // resource.use_resource();
}

#[test]
fn test_borrowing() {
    let resource = Resource::new("test");
    
    // 多次借用應該都可以工作
    fn use_resource_ref(r: &Resource) {
        r.use_resource();
    }
    
    use_resource_ref(&resource);
    use_resource_ref(&resource);
    
    // 原始變量仍然可用
    resource.use_resource();
}

#[test]
fn test_mutable_borrowing() {
    let mut resource = Resource::new("test");
    
    // 可變借用
    fn modify_resource(r: &mut Resource) {
        r.use_resource_mut();
    }
    
    modify_resource(&mut resource);
    
    // 借用結束後仍可使用
    resource.use_resource();
}

}

完整 Rust RESTful API 開發指南

📋 目錄

專案概述

這是一個使用 Rust 語言和 Axum 框架開發的完整 RESTful API 專案。專案展示了如何使用現代 Rust 技術棧構建高性能、類型安全的 web 服務。

技術棧

  • 語言: Rust 1.70+
  • 框架: Axum 0.7
  • 異步運行時: Tokio
  • 序列化: Serde
  • 日誌: Tracing
  • 測試: 內建測試框架

主要功能

  • 用戶管理(CRUD 操作)
  • 輸入驗證和錯誤處理
  • 結構化日誌記錄
  • 健康檢查端點
  • 完整的測試覆蓋

環境準備

系統要求

  • Rust 1.70 或更高版本
  • Cargo 包管理器
  • curl 或 HTTPie(用於測試)

安裝 Rust

# 安裝 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 更新到最新版本
rustup update

# 驗證安裝
rustc --version
cargo --version

創建專案

# 創建新專案
cargo new rust-restful-api
cd rust-restful-api

專案結構

rust-restful-api/
├── Cargo.toml              # 依賴配置
├── src/
│   └── main.rs            # 主程式
├── tests/                 # 測試檔案
│   └── integration_tests.rs
├── scripts/               # 腳本檔案
│   └── test_api.sh
├── docker/                # Docker 配置
│   └── Dockerfile
└── README.md              # 專案說明

核心代碼

1. Cargo.toml 配置

[package]
name = "rust-restful-api"
version = "0.1.0"
edition = "2021"
description = "A complete RESTful API example in Rust using Axum"

[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.0", features = ["v4", "serde"] }
tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1.0"
chrono = { version = "0.4", features = ["serde"] }
tower = "0.4"

[dev-dependencies]
tower-test = "0.4"

2. 主程式 (src/main.rs)

use axum::{
    extract::{Json, Path, State},
    http::StatusCode,
    response::Json as ResponseJson,
    routing::{delete, get, post, put},
    Router,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::net::TcpListener;
use tracing::{info, warn, Level};
use uuid::Uuid;

// 數據模型
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: Uuid,
    pub name: String,
    pub email: String,
    pub age: u32,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
    pub name: String,
    pub email: String,
    pub age: u32,
}

#[derive(Debug, Deserialize)]
pub struct UpdateUserRequest {
    pub name: Option<String>,
    pub email: Option<String>,
    pub age: Option<u32>,
}

// 響應模型
#[derive(Debug, Serialize)]
pub struct UserResponse {
    pub id: Uuid,
    pub name: String,
    pub email: String,
    pub age: u32,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Serialize)]
pub struct UsersListResponse {
    pub users: Vec<UserResponse>,
    pub total: usize,
}

#[derive(Debug, Serialize)]
pub struct ErrorResponse {
    pub error: String,
    pub code: u16,
    pub timestamp: DateTime<Utc>,
}

#[derive(Debug, Serialize)]
pub struct HealthResponse {
    pub status: String,
    pub timestamp: DateTime<Utc>,
    pub version: String,
}

// 應用狀態
pub type AppState = Arc<Mutex<HashMap<Uuid, User>>>;

// 輔助函數
impl User {
    fn new(name: String, email: String, age: u32) -> Self {
        let now = Utc::now();
        Self {
            id: Uuid::new_v4(),
            name,
            email,
            age,
            created_at: now,
            updated_at: now,
        }
    }
    
    fn update(&mut self, request: UpdateUserRequest) {
        if let Some(name) = request.name {
            self.name = name;
        }
        if let Some(email) = request.email {
            self.email = email;
        }
        if let Some(age) = request.age {
            self.age = age;
        }
        self.updated_at = Utc::now();
    }
}

impl From<User> for UserResponse {
    fn from(user: User) -> Self {
        Self {
            id: user.id,
            name: user.name,
            email: user.email,
            age: user.age,
            created_at: user.created_at,
            updated_at: user.updated_at,
        }
    }
}

// 錯誤處理
fn create_error_response(error: &str, code: u16) -> ErrorResponse {
    ErrorResponse {
        error: error.to_string(),
        code,
        timestamp: Utc::now(),
    }
}

// 輸入驗證
fn validate_create_user_request(request: &CreateUserRequest) -> Result<(), String> {
    if request.name.trim().is_empty() {
        return Err("Name cannot be empty".to_string());
    }
    if request.email.trim().is_empty() {
        return Err("Email cannot be empty".to_string());
    }
    if !request.email.contains('@') {
        return Err("Invalid email format".to_string());
    }
    if request.age > 150 {
        return Err("Age must be reasonable".to_string());
    }
    Ok(())
}

// API 處理器
pub async fn health_check() -> ResponseJson<HealthResponse> {
    ResponseJson(HealthResponse {
        status: "healthy".to_string(),
        timestamp: Utc::now(),
        version: env!("CARGO_PKG_VERSION").to_string(),
    })
}

pub async fn get_all_users(
    State(state): State<AppState>
) -> Result<ResponseJson<UsersListResponse>, (StatusCode, ResponseJson<ErrorResponse>)> {
    let users = state.lock().unwrap();
    let user_list: Vec<UserResponse> = users.values()
        .cloned()
        .map(UserResponse::from)
        .collect();
    
    let total = user_list.len();
    
    Ok(ResponseJson(UsersListResponse {
        users: user_list,
        total,
    }))
}

pub async fn get_user(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<ResponseJson<UserResponse>, (StatusCode, ResponseJson<ErrorResponse>)> {
    let users = state.lock().unwrap();
    
    match users.get(&id) {
        Some(user) => {
            info!("Retrieved user: {}", id);
            Ok(ResponseJson(UserResponse::from(user.clone())))
        }
        None => {
            warn!("User not found: {}", id);
            Err((
                StatusCode::NOT_FOUND,
                ResponseJson(create_error_response("User not found", 404)),
            ))
        }
    }
}

pub async fn create_user(
    State(state): State<AppState>,
    Json(payload): Json<CreateUserRequest>,
) -> Result<(StatusCode, ResponseJson<UserResponse>), (StatusCode, ResponseJson<ErrorResponse>)> {
    // 驗證輸入
    if let Err(error) = validate_create_user_request(&payload) {
        warn!("Invalid user creation request: {}", error);
        return Err((
            StatusCode::BAD_REQUEST,
            ResponseJson(create_error_response(&error, 400)),
        ));
    }

    // 檢查郵箱是否已存在
    {
        let users = state.lock().unwrap();
        if users.values().any(|u| u.email == payload.email) {
            return Err((
                StatusCode::CONFLICT,
                ResponseJson(create_error_response("Email already exists", 409)),
            ));
        }
    }

    let user = User::new(payload.name, payload.email, payload.age);
    let user_id = user.id;

    let mut users = state.lock().unwrap();
    users.insert(user_id, user.clone());

    info!("Created user: {} ({})", user.name, user_id);
    Ok((StatusCode::CREATED, ResponseJson(UserResponse::from(user))))
}

pub async fn update_user(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    Json(payload): Json<UpdateUserRequest>,
) -> Result<ResponseJson<UserResponse>, (StatusCode, ResponseJson<ErrorResponse>)> {
    // 檢查郵箱衝突(如果更新郵箱)
    if let Some(ref new_email) = payload.email {
        let users = state.lock().unwrap();
        if users.values().any(|u| u.id != id && u.email == *new_email) {
            return Err((
                StatusCode::CONFLICT,
                ResponseJson(create_error_response("Email already exists", 409)),
            ));
        }
    }
    
    let mut users = state.lock().unwrap();
    match users.get_mut(&id) {
        Some(user) => {
            user.update(payload);
            info!("Updated user: {}", id);
            Ok(ResponseJson(UserResponse::from(user.clone())))
        }
        None => {
            warn!("Attempted to update non-existent user: {}", id);
            Err((
                StatusCode::NOT_FOUND,
                ResponseJson(create_error_response("User not found", 404)),
            ))
        }
    }
}

pub async fn delete_user(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<StatusCode, (StatusCode, ResponseJson<ErrorResponse>)> {
    let mut users = state.lock().unwrap();
    
    match users.remove(&id) {
        Some(user) => {
            info!("Deleted user: {} ({})", user.name, id);
            Ok(StatusCode::NO_CONTENT)
        }
        None => {
            warn!("Attempted to delete non-existent user: {}", id);
            Err((
                StatusCode::NOT_FOUND,
                ResponseJson(create_error_response("User not found", 404)),
            ))
        }
    }
}

// 路由構建
pub fn create_router() -> Router {
    let state = Arc::new(Mutex::new(HashMap::new()));

    Router::new()
        .route("/health", get(health_check))
        .route("/api/users", get(get_all_users))
        .route("/api/users", post(create_user))
        .route("/api/users/:id", get(get_user))
        .route("/api/users/:id", put(update_user))
        .route("/api/users/:id", delete(delete_user))
        .with_state(state)
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 初始化日誌
    tracing_subscriber::fmt()
        .with_max_level(Level::INFO)
        .init();

    // 創建應用
    let app = create_router();

    // 啟動服務器
    let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string());
    let addr = format!("0.0.0.0:{}", port);
    
    info!("🚀 Starting server on {}", addr);
    info!("📚 Health check: http://localhost:{}/health", port);
    
    let listener = TcpListener::bind(&addr).await?;
    axum::serve(listener, app).await?;

    Ok(())
}

// 測試模組
#[cfg(test)]
mod tests {
    use super::*;
    use axum::http::{Method, Request};
    use axum::body::Body;
    use tower::ServiceExt;

    #[tokio::test]
    async fn test_health_check() {
        let app = create_router();
        
        let response = app
            .oneshot(
                Request::builder()
                    .method(Method::GET)
                    .uri("/health")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::OK);
    }

    #[tokio::test]
    async fn test_create_user() {
        let app = create_router();
        
        let user_data = serde_json::json!({
            "name": "John Doe",
            "email": "john@example.com",
            "age": 30
        });

        let response = app
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/users")
                    .header("content-type", "application/json")
                    .body(Body::from(user_data.to_string()))
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::CREATED);
    }

    #[tokio::test]
    async fn test_create_user_invalid_email() {
        let app = create_router();
        
        let user_data = serde_json::json!({
            "name": "John Doe",
            "email": "invalid-email",
            "age": 30
        });

        let response = app
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/users")
                    .header("content-type", "application/json")
                    .body(Body::from(user_data.to_string()))
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
    }

    #[tokio::test]
    async fn test_get_nonexistent_user() {
        let app = create_router();
        
        let response = app
            .oneshot(
                Request::builder()
                    .method(Method::GET)
                    .uri("/api/users/00000000-0000-0000-0000-000000000000")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::NOT_FOUND);
    }

    #[tokio::test]
    async fn test_email_uniqueness() {
        let app = create_router();
        
        // 創建第一個用戶
        let user1_data = serde_json::json!({
            "name": "User One",
            "email": "duplicate@example.com",
            "age": 30
        });
        
        let response1 = app
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/users")
                    .header("content-type", "application/json")
                    .body(Body::from(user1_data.to_string()))
                    .unwrap(),
            )
            .await
            .unwrap();
        
        assert_eq!(response1.status(), StatusCode::CREATED);
        
        // 嘗試創建具有相同電子郵件的第二個用戶
        let user2_data = serde_json::json!({
            "name": "User Two",
            "email": "duplicate@example.com",
            "age": 25
        });
        
        let response2 = app
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/users")
                    .header("content-type", "application/json")
                    .body(Body::from(user2_data.to_string()))
                    .unwrap(),
            )
            .await
            .unwrap();
        
        assert_eq!(response2.status(), StatusCode::CONFLICT);
    }
}

功能特色

1. 🔒 類型安全

  • 強類型系統: 使用 Rust 的類型系統確保編譯時安全
  • 序列化保證: Serde 自動處理 JSON 序列化/反序列化
  • UUID 支持: 唯一標識符保證資料完整性

2. ⚡ 高性能

  • 異步處理: 基於 Tokio 的異步運行時
  • 零成本抽象: Axum 框架提供高效的請求處理
  • 內存安全: 無垃圾回收,低延遲響應

3. 🛡️ 錯誤處理

  • 統一錯誤格式: 所有錯誤響應都包含錯誤碼和時間戳
  • 輸入驗證: 全面的請求數據驗證
  • 適當的 HTTP 狀態碼: 符合 REST 標準的響應碼

4. 📊 日誌記錄

  • 結構化日誌: 使用 tracing 記錄詳細的操作信息
  • 可配置級別: 支持不同的日誌級別
  • 性能追蹤: 異步友好的日誌記錄

5. 🧪 測試覆蓋

  • 單元測試: 涵蓋所有主要功能
  • 集成測試: 測試完整的 API 流程
  • 邊界情況: 包含錯誤情況的測試

API 文檔

基本信息

  • Base URL: http://localhost:3000
  • Content-Type: application/json
  • 認證: 無(示例項目)

端點列表

1. 健康檢查

GET /health

響應示例:

{
  "status": "healthy",
  "timestamp": "2025-07-17T10:30:00.000Z",
  "version": "0.1.0"
}

2. 獲取所有用戶

GET /api/users

響應示例:

{
  "users": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "張三",
      "email": "zhangsan@example.com",
      "age": 25,
      "created_at": "2025-07-17T10:30:00.000Z",
      "updated_at": "2025-07-17T10:30:00.000Z"
    }
  ],
  "total": 1
}

3. 創建用戶

POST /api/users

請求體:

{
  "name": "張三",
  "email": "zhangsan@example.com",
  "age": 25
}

響應示例 (201 Created):

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "張三",
  "email": "zhangsan@example.com",
  "age": 25,
  "created_at": "2025-07-17T10:30:00.000Z",
  "updated_at": "2025-07-17T10:30:00.000Z"
}

4. 獲取單個用戶

GET /api/users/{id}

路徑參數:

  • id: 用戶 UUID

響應示例:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "張三",
  "email": "zhangsan@example.com",
  "age": 25,
  "created_at": "2025-07-17T10:30:00.000Z",
  "updated_at": "2025-07-17T10:30:00.000Z"
}

5. 更新用戶

PUT /api/users/{id}

請求體 (所有字段都是可選的):

{
  "name": "李四",
  "email": "lisi@example.com",
  "age": 30
}

響應示例:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "李四",
  "email": "lisi@example.com",
  "age": 30,
  "created_at": "2025-07-17T10:30:00.000Z",
  "updated_at": "2025-07-17T11:00:00.000Z"
}

6. 刪除用戶

DELETE /api/users/{id}

響應: 204 No Content

錯誤響應格式

所有錯誤響應都遵循以下格式:

{
  "error": "錯誤訊息",
  "code": 404,
  "timestamp": "2025-07-17T10:30:00.000Z"
}

HTTP 狀態碼

狀態碼說明
200OK - 成功獲取資源
201Created - 成功創建資源
204No Content - 成功刪除資源
400Bad Request - 請求格式錯誤
404Not Found - 資源不存在
409Conflict - 資源衝突(如重複郵箱)
500Internal Server Error - 服務器內部錯誤

測試指南

運行項目

# 編譯並運行
cargo run

# 在背景運行
cargo run &

# 指定端口
PORT=8080 cargo run

單元測試

# 運行所有測試
cargo test

# 運行特定測試
cargo test test_health_check

# 顯示測試輸出
cargo test -- --nocapture

# 運行測試並顯示覆蓋率
cargo test --verbose

手動測試

使用 curl

# 1. 健康檢查
curl http://localhost:3000/health

# 2. 創建用戶
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{
    "name": "張三",
    "email": "zhangsan@example.com",
    "age": 25
  }'

# 3. 獲取所有用戶
curl http://localhost:3000/api/users

# 4. 獲取單個用戶 (替換 UUID)
curl http://localhost:3000/api/users/550e8400-e29b-41d4-a716-446655440000

# 5. 更新用戶
curl -X PUT http://localhost:3000/api/users/550e8400-e29b-41d4-a716-446655440000 \
  -H "Content-Type: application/json" \
  -d '{
    "name": "李四",
    "age": 30
  }'

# 6. 刪除用戶
curl -X DELETE http://localhost:3000/api/users/550e8400-e29b-41d4-a716-446655440000

使用 HTTPie

# 安裝 HTTPie
pip install httpie

# 創建用戶
http POST localhost:3000/api/users name="王五" email="wangwu@example.com" age:=28

# 獲取用戶
http GET localhost:3000/api/users

# 更新用戶
http PUT localhost:3000/api/users/USER_ID name="趙六"

# 刪除用戶
http DELETE localhost:3000/api/users/USER_ID

自動化測試腳本

創建 scripts/test_api.sh:

#!/bin/bash

# 顏色定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

BASE_URL="http://localhost:3000"
PASSED=0
FAILED=0

# 測試函數
test_endpoint() {
    local test_name="$1"
    local expected_code="$2"
    local actual_code="$3"
    
    echo -e "\n🧪 測試: $test_name"
    echo "期望狀態碼: $expected_code"
    echo "實際狀態碼: $actual_code"
    
    if [ "$actual_code" = "$expected_code" ]; then
        echo -e "${GREEN}✅ 通過${NC}"
        ((PASSED++))
    else
        echo -e "${RED}❌ 失敗${NC}"
        ((FAILED++))
    fi
}

echo -e "${YELLOW}🚀 開始測試 Rust RESTful API${NC}"

# 檢查服務器
if ! curl -s "$BASE_URL/health" > /dev/null; then
    echo -e "${RED}❌ 服務器未運行${NC}"
    exit 1
fi

# 1. 健康檢查
echo -e "\n1️⃣ 健康檢查"
response=$(curl -s -w "%{http_code}" -o /tmp/health_response "$BASE_URL/health")
status_code="${response: -3}"
test_endpoint "健康檢查" "200" "$status_code"

# 2. 創建用戶
echo -e "\n2️⃣ 創建用戶"
response=$(curl -s -w "%{http_code}" -o /tmp/create_response \
    -X POST "$BASE_URL/api/users" \
    -H "Content-Type: application/json" \
    -d '{"name":"張三","email":"zhangsan@example.com","age":25}')
status_code="${response: -3}"
test_endpoint "創建用戶" "201" "$status_code"

# 提取用戶 ID
USER_ID=$(cat /tmp/create_response | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
echo -e "${YELLOW}用戶 ID: $USER_ID${NC}"

# 3. 獲取所有用戶
echo -e "\n3️⃣ 獲取所有用戶"
response=$(curl -s -w "%{http_code}" -o /tmp/users_response "$BASE_URL/api/users")
status_code="${response: -3}"
test_endpoint "獲取所有用戶" "200" "$status_code"

# 4. 獲取單個用戶
if [ -n "$USER_ID" ]; then
    echo -e "\n4️⃣ 獲取單個用戶"
    response=$(curl -s -w "%{http_code}" -o /tmp/user_response "$BASE_URL/api/users/$USER_ID")
    status_code="${response: -3}"
    test_endpoint "獲取單個用戶" "200" "$status_code"
    
    # 5. 更新用戶
    echo -e "\n5️⃣ 更新用戶"
    response=$(curl -s -w "%{http_code}" -o /tmp/update_response \
        -X PUT "$BASE_URL/api/users/$USER_ID" \
        -H "Content-Type: application/json" \
        -d '{"name":"李四","age":30}')
    status_code="${response: -3}"
    test_endpoint "更新用戶" "200" "$status_code"
    
    # 6. 刪除用戶
    echo -e "\n6️⃣ 刪除用戶"
    response=$(curl -s -w "%{http_code}" -o /tmp/delete_response \
        -X DELETE "$BASE_URL/api/users/$USER_ID")
    status_code="${response: -3}"
    test_endpoint "刪除用戶" "204" "$status_code"

Rust selfSelf 與 C++ this 對比指南

📋 目錄

概念概覽

基本定義

  • Rust Self: 當前類型的別名,用於類型註解
  • Rust self: 當前實例,用於訪問實例成員
  • C++ this: 指向當前對象的指針

快速對比

#![allow(unused)]
fn main() {
// Rust
impl User {
    fn new() -> Self {          // Self = 類型別名
        Self { ... }            // 構造當前類型
    }
    
    fn method(&self) {          // self = 實例參數
        self.field;             // 訪問實例成員
    }
}
}
// C++
class User {
public:
    User() {
        this->field = value;    // this = 對象指針
    }
    
    void method() {
        this->field;            // 訪問成員
    }
};

基本對比

語法對比表

特性Rust SelfRust selfC++ this
本質類型別名實例/引用對象指針
使用場景返回類型、類型註解方法參數訪問成員
必須性可選方法中必須可選
語法-> Self&self, &mut self, selfthis->member

基本範例

Rust 版本

#![allow(unused)]
fn main() {
struct User {
    name: String,
    age: u32,
}

impl User {
    // Self 作為類型別名
    fn new(name: String, age: u32) -> Self {
        Self { name, age }
    }
    
    // self 作為實例引用
    fn get_name(&self) -> &str {
        &self.name
    }
    
    fn set_age(&mut self, age: u32) {
        self.age = age;
    }
}
}

C++ 版本

class User {
private:
    std::string name;
    int age;
    
public:
    // 構造函數
    User(const std::string& name, int age) : name(name), age(age) {}
    
    // const 方法
    const std::string& get_name() const {
        return this->name;  // 或直接 return name;
    }
    
    // 非 const 方法
    void set_age(int age) {
        this->age = age;    // 或直接 this->age = age;
    }
};

詳細解析

1. Rust Self - 類型別名

基本用法

#![allow(unused)]
fn main() {
struct Point {
    x: f64,
    y: f64,
}

impl Point {
    // Self 等同於 Point
    fn new(x: f64, y: f64) -> Self {
        Self { x, y }
    }
    
    // 返回類型使用 Self
    fn origin() -> Self {
        Self { x: 0.0, y: 0.0 }
    }
    
    // 參數類型使用 Self
    fn distance(&self, other: &Self) -> f64 {
        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
    }
}
}

泛型中的 Self

#![allow(unused)]
fn main() {
trait Clone {
    fn clone(&self) -> Self;  // Self 代表實現該 trait 的類型
}

impl Clone for Point {
    fn clone(&self) -> Self {  // 這裡 Self = Point
        Self {
            x: self.x,
            y: self.y,
        }
    }
}
}

2. Rust self - 實例引用

三種形式的 self

&self - 不可變借用
#![allow(unused)]
fn main() {
impl User {
    fn get_info(&self) -> String {
        format!("{} is {} years old", self.name, self.age)
    }
    
    fn is_adult(&self) -> bool {
        self.age >= 18
    }
}
}
// C++ 等價 - const 方法
class User {
public:
    std::string get_info() const {
        return name + " is " + std::to_string(age) + " years old";
    }
    
    bool is_adult() const {
        return age >= 18;
    }
};
&mut self - 可變借用
#![allow(unused)]
fn main() {
impl User {
    fn update_name(&mut self, new_name: String) {
        self.name = new_name;
    }
    
    fn increment_age(&mut self) {
        self.age += 1;
    }
}
}
// C++ 等價 - 非 const 方法
class User {
public:
    void update_name(const std::string& new_name) {
        this->name = new_name;
    }
    
    void increment_age() {
        this->age++;
    }
};
self - 取得所有權
#![allow(unused)]
fn main() {
impl User {
    fn into_string(self) -> String {
        format!("{} ({})", self.name, self.age)
    }
    
    fn consume_and_create_new(self, new_name: String) -> User {
        User {
            name: new_name,
            age: self.age,
        }
    }
}
}
// C++ 等價 - 移動語義或右值引用
class User {
public:
    std::string into_string() && {  // 右值引用方法
        return name + " (" + std::to_string(age) + ")";
    }
    
    User consume_and_create_new(std::string new_name) && {
        return User(std::move(new_name), age);
    }
};

3. C++ this - 對象指針

基本用法

class User {
private:
    std::string name;
    int age;
    
public:
    User(const std::string& name, int age) {
        this->name = name;  // 明確使用 this
        this->age = age;
    }
    
    // 返回 this 指針
    User* set_name(const std::string& name) {
        this->name = name;
        return this;
    }
    
    // 返回 this 引用
    User& set_age(int age) {
        this->age = age;
        return *this;
    }
    
    // 比較操作
    bool operator==(const User& other) const {
        return this->name == other.name && this->age == other.age;
    }
};

鏈式調用

// 使用
User user("Alice", 25);
user.set_name("Bob").set_age(30);  // 鏈式調用

實用範例

完整的用戶管理系統

Rust 版本

#[derive(Debug, Clone)]
struct User {
    id: u32,
    name: String,
    email: String,
    age: u32,
}

impl User {
    // 使用 Self 作為返回類型
    fn new(id: u32, name: String, email: String, age: u32) -> Self {
        Self { id, name, email, age }
    }
    
    // 使用 Self 作為參數類型
    fn from_other(other: &Self, new_id: u32) -> Self {
        Self {
            id: new_id,
            name: other.name.clone(),
            email: other.email.clone(),
            age: other.age,
        }
    }
    
    // &self - 讀取操作
    fn get_display_name(&self) -> String {
        format!("{} <{}>", self.name, self.email)
    }
    
    fn is_valid(&self) -> bool {
        !self.name.is_empty() && 
        self.email.contains('@') && 
        self.age > 0
    }
    
    // &mut self - 修改操作
    fn update_email(&mut self, new_email: String) -> Result<(), String> {
        if new_email.contains('@') {
            self.email = new_email;
            Ok(())
        } else {
            Err("Invalid email format".to_string())
        }
    }
    
    fn birthday(&mut self) {
        self.age += 1;
    }
    
    // self - 消費操作
    fn into_summary(self) -> String {
        format!("User {} (ID: {}, Age: {})", self.name, self.id, self.age)
    }
    
    fn merge_with(self, other: Self) -> Self {
        Self {
            id: self.id,
            name: format!("{} & {}", self.name, other.name),
            email: self.email,
            age: (self.age + other.age) / 2,
        }
    }
}

// 使用範例
fn main() {
    let mut user1 = User::new(1, "Alice".to_string(), "alice@example.com".to_string(), 25);
    let user2 = User::from_other(&user1, 2);
    
    println!("{}", user1.get_display_name());
    user1.birthday();
    user1.update_email("alice.new@example.com".to_string()).unwrap();
    
    let summary = user1.into_summary();
    println!("{}", summary);
}

C++ 版本

#include <iostream>
#include <string>
#include <stdexcept>

class User {
private:
    uint32_t id;
    std::string name;
    std::string email;
    uint32_t age;
    
public:
    // 構造函數
    User(uint32_t id, const std::string& name, const std::string& email, uint32_t age)
        : id(id), name(name), email(email), age(age) {}
    
    // 拷貝構造函數
    User(const User& other, uint32_t new_id)
        : id(new_id), name(other.name), email(other.email), age(other.age) {}
    
    // const 方法 - 讀取操作
    std::string get_display_name() const {
        return this->name + " <" + this->email + ">";
    }
    
    bool is_valid() const {
        return !this->name.empty() && 
               this->email.find('@') != std::string::npos && 
               this->age > 0;
    }
    
    // 非 const 方法 - 修改操作
    void update_email(const std::string& new_email) {
        if (new_email.find('@') != std::string::npos) {
            this->email = new_email;
        } else {
            throw std::invalid_argument("Invalid email format");
        }
    }
    
    void birthday() {
        this->age++;
    }
    
    // 返回 this 引用,支持鏈式調用
    User& set_name(const std::string& new_name) {
        this->name = new_name;
        return *this;
    }
    
    User& set_age(uint32_t new_age) {
        this->age = new_age;
        return *this;
    }
    
    // 消費操作(移動語義)
    std::string into_summary() && {
        return "User " + name + " (ID: " + std::to_string(id) + ", Age: " + std::to_string(age) + ")";
    }
    
    User merge_with(User&& other) && {
        return User(
            this->id,
            this->name + " & " + other.name,
            this->email,
            (this->age + other.age) / 2
        );
    }
};

// 使用範例
int main() {
    User user1(1, "Alice", "alice@example.com", 25);
    User user2(user1, 2);
    
    std::cout << user1.get_display_name() << std::endl;
    user1.birthday();
    user1.update_email("alice.new@example.com");
    
    // 鏈式調用
    user1.set_name("Alice Smith").set_age(26);
    
    std::string summary = std::move(user1).into_summary();
    std::cout << summary << std::endl;
    
    return 0;
}

進階應用

1. 特徵中的 Self

Rust 特徵

#![allow(unused)]
fn main() {
trait Drawable {
    fn draw(&self);
    fn clone_drawable(&self) -> Self;  // Self 代表實現者的類型
}

trait Builder {
    type Item;
    fn build(self) -> Self::Item;  // 關聯類型
}

struct Circle {
    radius: f64,
}

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing circle with radius {}", self.radius);
    }
    
    fn clone_drawable(&self) -> Self {  // Self = Circle
        Self {
            radius: self.radius,
        }
    }
}

impl Builder for Circle {
    type Item = String;
    
    fn build(self) -> Self::Item {
        format!("Circle with radius {}", self.radius)
    }
}
}

C++ 等價

template<typename T>
class Drawable {
public:
    virtual void draw() const = 0;
    virtual T clone_drawable() const = 0;
    virtual ~Drawable() = default;
};

template<typename T>
class Builder {
public:
    using Item = T;
    virtual Item build() = 0;
    virtual ~Builder() = default;
};

class Circle : public Drawable<Circle>, public Builder<std::string> {
private:
    double radius;
    
public:
    Circle(double radius) : radius(radius) {}
    
    void draw() const override {
        std::cout << "Drawing circle with radius " << radius << std::endl;
    }
    
    Circle clone_drawable() const override {
        return Circle(this->radius);
    }
    
    std::string build() override {
        return "Circle with radius " + std::to_string(this->radius);
    }
};

2. 泛型中的 Self 和 this

Rust 泛型

#![allow(unused)]
fn main() {
trait Comparable<T> {
    fn compare(&self, other: &T) -> std::cmp::Ordering;
}

impl<T> Comparable<T> for T 
where 
    T: PartialOrd<T> 
{
    fn compare(&self, other: &T) -> std::cmp::Ordering {
        self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
    }
}

struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn new(x: f64, y: f64) -> Self {
        Self { x, y }
    }
    
    fn distance_to(&self, other: &Self) -> f64 {
        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
    }
}
}

C++ 泛型

template<typename T>
class Comparable {
public:
    virtual int compare(const T& other) const = 0;
    virtual ~Comparable() = default;
};

template<typename T>
class Point : public Comparable<Point<T>> {
private:
    T x, y;
    
public:
    Point(T x, T y) : x(x), y(y) {}
    
    static Point<T> new_point(T x, T y) {
        return Point<T>(x, y);
    }
    
    T distance_to(const Point<T>& other) const {
        T dx = this->x - other.x;
        T dy = this->y - other.y;
        return std::sqrt(dx * dx + dy * dy);
    }
    
    int compare(const Point<T>& other) const override {
        T this_dist = this->distance_to(Point<T>(0, 0));
        T other_dist = other.distance_to(Point<T>(0, 0));
        
        if (this_dist < other_dist) return -1;
        if (this_dist > other_dist) return 1;
        return 0;
    }
};

常見錯誤

1. Rust 常見錯誤

錯誤:混淆 Self 和 self

#![allow(unused)]
fn main() {
// ❌ 錯誤
impl User {
    fn new() -> self {  // 應該是 Self
        self { ... }    // 應該是 Self
    }
}

// ✅ 正確
impl User {
    fn new() -> Self {
        Self { ... }
    }
}
}

錯誤:忘記 self 參數

#![allow(unused)]
fn main() {
// ❌ 錯誤
impl User {
    fn get_name() -> &str {  // 缺少 &self
        &name               // 無法訪問 self.name
    }
}

// ✅ 正確
impl User {
    fn get_name(&self) -> &str {
        &self.name
    }
}
}

錯誤:錯誤的 self 類型

#![allow(unused)]
fn main() {
// ❌ 錯誤
impl User {
    fn update_name(self, name: String) {  // 應該是 &mut self
        self.name = name;  // 無法修改 moved value
    }
}

// ✅ 正確
impl User {
    fn update_name(&mut self, name: String) {
        self.name = name;
    }
}
}

2. C++ 常見錯誤

錯誤:不必要的 this

// ❌ 冗餘但不錯誤
class User {
    std::string name;
public:
    void set_name(const std::string& name) {
        this->name = name;  // 參數名衝突時必須用 this
    }
};

// ✅ 更好的做法
class User {
    std::string name;
public:
    void set_name(const std::string& new_name) {
        name = new_name;  // 沒有衝突,可以省略 this
    }
};

錯誤:返回 this 的錯誤類型

// ❌ 錯誤
class User {
public:
    User set_name(const std::string& name) {  // 返回拷貝
        this->name = name;
        return *this;  // 效率低
    }
};

// ✅ 正確
class User {
public:
    User& set_name(const std::string& name) {  // 返回引用
        this->name = name;
        return *this;
    }
};

最佳實踐

1. Rust 最佳實踐

構造函數使用 Self

#![allow(unused)]
fn main() {
impl User {
    // ✅ 推薦:使用 Self
    fn new(name: String, age: u32) -> Self {
        Self { name, age }
    }
    
    // ❌ 不推薦:重複類型名
    fn new_verbose(name: String, age: u32) -> User {
        User { name, age }
    }
}
}

選擇合適的 self 類型

#![allow(unused)]
fn main() {
impl User {
    // 只讀操作使用 &self
    fn get_name(&self) -> &str {
        &self.name
    }
    
    // 修改操作使用 &mut self
    fn set_name(&mut self, name: String) {
        self.name = name;
    }
    
    // 消費操作使用 self
    fn into_display(self) -> String {
        format!("{} ({})", self.name, self.age)
    }
}
}

鏈式調用

#![allow(unused)]
fn main() {
impl User {
    fn set_name(mut self, name: String) -> Self {
        self.name = name;
        self
    }
    
    fn set_age(mut self, age: u32) -> Self {
        self.age = age;
        self
    }
}

// 使用
let user = User::new("Alice".to_string(), 25)
    .set_name("Bob".to_string())
    .set_age(30);
}

2. C++ 最佳實踐

避免不必要的 this

class User {
private:
    std::string name;
    int age;
    
public:
    // ✅ 推薦:沒有衝突時省略 this
    void set_age(int new_age) {
        age = new_age;
    }
    
    // ✅ 必要:參數名衝突時使用 this
    void set_name(const std::string& name) {
        this->name = name;
    }
};

返回引用支持鏈式調用

class User {
public:
    User& set_name(const std::string& name) {
        this->name = name;
        return *this;  // 返回引用
    }
    
    User& set_age(int age) {
        this->age = age;
        return *this;
    }
};

// 使用
User user;
user.set_name("Alice").set_age(25);

總結對照表

完整對比

特性Rust SelfRust selfC++ this
定義當前類型的別名當前實例指向當前對象的指針
類型類型實例/引用指針
語法Self&self, &mut self, selfthis->
使用場景返回類型、參數類型方法第一參數訪問成員
必須性可選(可用具體類型)實例方法中必須可選(無衝突時)
性能編譯時解析零成本運行時解引用

功能對應

功能RustC++
構造函數fn new() -> SelfClassName()
實例方法fn method(&self)void method()
修改方法fn method(&mut self)void method()
消費方法fn method(self)void method() &&
鏈式調用fn method(self) -> SelfClass& method()
類型別名SelfClassName

使用建議

何時使用 Rust Self

  • ✅ 構造函數返回類型
  • ✅ 參數類型註解
  • ✅ 泛型約束
  • ✅ 特徵定義

何時使用 Rust self

  • ✅ 所有實例方法
  • ✅ 根據需要選擇 &self&mut selfself
  • ✅ 鏈式調用

何時使用 C++ this

  • ✅ 參數名衝突時
  • ✅ 返回自身引用/指針
  • ✅ 模板消除歧義時
  • ❌ 一般情況下可省略

記憶口訣

Rust:

  • Self = 類型的我 (Type Me)
  • self = 實例的我 (Instance Me)

C++:

  • this = 指向我的指針 (Pointer to Me)

這份指南涵蓋了 Rust 和 C++ 中處理 "自我引用" 的所有重要概念。通過對比學習,可以更好地理解兩種語言的設計哲學和實際應用。

Rust Derive Traits 比較:自動生成 vs 手動實作

概述

在 Rust 中,#[derive(...)] 屬性可以自動為結構體生成常用的 trait 實作,大幅減少重複程式碼。本文比較使用 derive 和手動實作的差異。

專案設定

在開始之前,需要建立專案並設定 Cargo.toml

cargo new derive_comparison
cd derive_comparison

Cargo.toml

[package]
name = "derive_comparison"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.0", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }

方法一:使用 #[derive(Debug, Clone, Serialize, Deserialize)]

完整程式碼

use serde::{Serialize, Deserialize};
use uuid::Uuid;
use chrono::{DateTime, Utc};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: Uuid,
    pub name: String,
    pub email: String,
    pub age: u32,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

fn main() {
    // 建立一個 User 實例
    let user = User {
        id: Uuid::new_v4(),
        name: "John Doe".to_string(),
        email: "john@example.com".to_string(),
        age: 30,
        created_at: Utc::now(),
        updated_at: Utc::now(),
    };
    
    println!("=== 使用 derive 的範例 ===\n");
    
    // Debug - 直接可用
    println!("Debug 輸出:");
    println!("{:?}", user);
    println!();
    
    // Clone - 直接可用
    let user2 = user.clone();
    println!("Clone 成功:");
    println!("原始 user: {:?}", user);
    println!("複製 user2: {:?}", user2);
    println!();
    
    // Serialize - 直接可用
    let json = serde_json::to_string(&user).unwrap();
    println!("Serialize 到 JSON:");
    println!("{}", json);
    println!();
    
    // Deserialize - 直接可用
    let user3: User = serde_json::from_str(&json).unwrap();
    println!("Deserialize 從 JSON:");
    println!("{:?}", user3);
    println!();
    
    // 驗證序列化/反序列化是否正確
    println!("驗證序列化/反序列化:");
    println!("原始 user ID: {}", user.id);
    println!("反序列化 user3 ID: {}", user3.id);
    println!("ID 是否相同: {}", user.id == user3.id);
}

特點

  • 程式碼量:僅需 1 行 derive 屬性
  • 維護性:自動生成,不需要手動維護
  • 一致性:標準實作,不易出錯
  • 開發效率:立即可用,無需額外實作

不使用 #[derive(...)] - 手動實作

程式碼

use serde::{Serialize, Deserialize, Serializer, Deserializer};
use uuid::Uuid;
use chrono::{DateTime, Utc};

// 只有基本結構體
pub struct User {
    pub id: Uuid,
    pub name: String,
    pub email: String,
    pub age: u32,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

// 需要手動實作 Debug
impl std::fmt::Debug for User {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("User")
            .field("id", &self.id)
            .field("name", &self.name)
            .field("email", &self.email)
            .field("age", &self.age)
            .field("created_at", &self.created_at)
            .field("updated_at", &self.updated_at)
            .finish()
    }
}

// 需要手動實作 Clone
impl Clone for User {
    fn clone(&self) -> Self {
        User {
            id: self.id.clone(),
            name: self.name.clone(),
            email: self.email.clone(),
            age: self.age,
            created_at: self.created_at,
            updated_at: self.updated_at,
        }
    }
}

// 需要手動實作 Serialize
impl Serialize for User {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        use serde::ser::SerializeStruct;
        let mut state = serializer.serialize_struct("User", 6)?;
        state.serialize_field("id", &self.id)?;
        state.serialize_field("name", &self.name)?;
        state.serialize_field("email", &self.email)?;
        state.serialize_field("age", &self.age)?;
        state.serialize_field("created_at", &self.created_at)?;
        state.serialize_field("updated_at", &self.updated_at)?;
        state.end()
    }
}

// 需要手動實作 Deserialize
impl<'de> Deserialize<'de> for User {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        use serde::de::{self, Visitor, MapAccess};
        use std::fmt;

        #[derive(Deserialize)]
        #[serde(field_identifier, rename_all = "lowercase")]
        enum Field { Id, Name, Email, Age, CreatedAt, UpdatedAt }

        struct UserVisitor;

        impl<'de> Visitor<'de> for UserVisitor {
            type Value = User;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("struct User")
            }

            fn visit_map<V>(self, mut map: V) -> Result<User, V::Error>
            where
                V: MapAccess<'de>,
            {
                let mut id = None;
                let mut name = None;
                let mut email = None;
                let mut age = None;
                let mut created_at = None;
                let mut updated_at = None;

                while let Some(key) = map.next_key()? {
                    match key {
                        Field::Id => {
                            if id.is_some() {
                                return Err(de::Error::duplicate_field("id"));
                            }
                            id = Some(map.next_value()?);
                        }
                        Field::Name => {
                            if name.is_some() {
                                return Err(de::Error::duplicate_field("name"));
                            }
                            name = Some(map.next_value()?);
                        }
                        Field::Email => {
                            if email.is_some() {
                                return Err(de::Error::duplicate_field("email"));
                            }
                            email = Some(map.next_value()?);
                        }
                        Field::Age => {
                            if age.is_some() {
                                return Err(de::Error::duplicate_field("age"));
                            }
                            age = Some(map.next_value()?);
                        }
                        Field::CreatedAt => {
                            if created_at.is_some() {
                                return Err(de::Error::duplicate_field("created_at"));
                            }
                            created_at = Some(map.next_value()?);
                        }
                        Field::UpdatedAt => {
                            if updated_at.is_some() {
                                return Err(de::Error::duplicate_field("updated_at"));
                            }
                            updated_at = Some(map.next_value()?);
                        }
                    }
                }

                let id = id.ok_or_else(|| de::Error::missing_field("id"))?;
                let name = name.ok_or_else(|| de::Error::missing_field("name"))?;
                let email = email.ok_or_else(|| de::Error::missing_field("email"))?;
                let age = age.ok_or_else(|| de::Error::missing_field("age"))?;
                let created_at = created_at.ok_or_else(|| de::Error::missing_field("created_at"))?;
                let updated_at = updated_at.ok_or_else(|| de::Error::missing_field("updated_at"))?;

                Ok(User {
                    id,
                    name,
                    email,
                    age,
                    created_at,
                    updated_at,
                })
            }
        }

        const FIELDS: &'static [&'static str] = &["id", "name", "email", "age", "created_at", "updated_at"];
        deserializer.deserialize_struct("User", FIELDS, UserVisitor)
    }
}

// 使用範例
fn main() {
    let user = User {
        id: Uuid::new_v4(),
        name: "John Doe".to_string(),
        email: "john@example.com".to_string(),
        age: 30,
        created_at: Utc::now(),
        updated_at: Utc::now(),
    };
    
    // 現在這些功能都可以使用了
    println!("{:?}", user);
    let user2 = user.clone();
    let json = serde_json::to_string(&user).unwrap();
    let user3: User = serde_json::from_str(&json).unwrap();
}

特點

  • 程式碼量:需要數十行手動實作
  • 複雜性:需要深入了解 trait 的實作細節
  • 維護成本:每次修改結構體都需要更新所有實作
  • 錯誤風險:容易在手動實作中出現錯誤

執行結果比較

方法一(derive)執行結果

=== 使用 derive 的範例 ===

Debug 輸出:
User { id: a1b2c3d4-e5f6-7890-abcd-ef1234567890, name: "John Doe", email: "john@example.com", age: 30, created_at: 2025-01-20T10:30:00.123456789Z, updated_at: 2025-01-20T10:30:00.123456789Z }

Clone 成功:
原始 user: User { id: a1b2c3d4-e5f6-7890-abcd-ef1234567890, name: "John Doe", email: "john@example.com", age: 30, created_at: 2025-01-20T10:30:00.123456789Z, updated_at: 2025-01-20T10:30:00.123456789Z }
複製 user2: User { id: a1b2c3d4-e5f6-7890-abcd-ef1234567890, name: "John Doe", email: "john@example.com", age: 30, created_at: 2025-01-20T10:30:00.123456789Z, updated_at: 2025-01-20T10:30:00.123456789Z }

Serialize 到 JSON:
{"id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","name":"John Doe","email":"john@example.com","age":30,"created_at":"2025-01-20T10:30:00.123456789Z","updated_at":"2025-01-20T10:30:00.123456789Z"}

Deserialize 從 JSON:
User { id: a1b2c3d4-e5f6-7890-abcd-ef1234567890, name: "John Doe", email: "john@example.com", age: 30, created_at: 2025-01-20T10:30:00.123456789Z, updated_at: 2025-01-20T10:30:00.123456789Z }

驗證序列化/反序列化:
原始 user ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
反序列化 user3 ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
ID 是否相同: true

方法二(手動實作)執行結果

=== 手動實作的 traits 測試 ===

1. Debug 輸出:
User { id: a1b2c3d4-e5f6-7890-abcd-ef1234567890, name: "John Doe", email: "john@example.com", age: 30, created_at: 2025-01-20T10:30:00.123456789Z, updated_at: 2025-01-20T10:30:00.123456789Z }

2. Clone 測試:
原始 user name: John Doe
複製 user2 name: John Doe
記憶體位址是否不同: true

3. Serialize 測試:
序列化成功:
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "name": "John Doe",
  "email": "john@example.com",
  "age": 30,
  "created_at": "2025-01-20T10:30:00.123456789Z",
  "updated_at": "2025-01-20T10:30:00.123456789Z"
}

4. Deserialize 測試:
反序列化成功:
原始 user ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
反序列化 user3 ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
ID 是否相同: true
姓名是否相同: true

=== 所有測試完成 ===
手動實作的程式碼行數: 約 150+ 行
使用 #[derive(...)] 只需要: 1 行

詳細實作解析

Debug Trait 實作

  • derive 版本:自動生成標準格式的 Debug 輸出
  • 手動版本:使用 debug_struct 手動建構格式化字串

Clone Trait 實作

  • derive 版本:自動為每個欄位呼叫 clone() 方法
  • 手動版本:手動處理每個欄位的複製邏輯

Serialize Trait 實作

  • derive 版本:自動序列化所有欄位
  • 手動版本:使用 SerializeStruct 逐一序列化每個欄位

Deserialize Trait 實作(最複雜)

  • derive 版本:自動處理所有反序列化邏輯
  • 手動版本:需要實作:
    • Field enum 用於欄位識別
    • Visitor struct 用於處理反序列化邏輯
    • 重複欄位檢查
    • 缺失欄位檢查
    • 型別轉換和錯誤處理

比較總結

項目使用 derive手動實作
程式碼量1 行150+ 行
開發時間幾秒鐘數小時
維護成本極低
錯誤風險極低
客製化程度有限完全客製化
學習曲線簡單困難
編譯時間相對慢
功能一致性標準實作可能不一致

使用建議

建議使用 derive 的情況

  • 一般開發:大多數情況下都適用
  • 快速原型:需要快速實作基本功能
  • 標準需求:不需要特殊的客製化邏輯
  • 團隊協作:保持程式碼一致性

考慮手動實作的情況

  • 特殊需求:需要客製化的序列化格式
  • 效能最佳化:需要特定的效能優化
  • 學習目的:深入理解 Rust trait 系統
  • 複雜邏輯:需要特殊的驗證或轉換邏輯

進階技巧

條件式 derive

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct User {
    // ...
}
}

自訂 derive 行為

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: Uuid,
    
    #[serde(rename = "full_name")]
    pub name: String,
    
    #[serde(skip_serializing_if = "Option::is_none")]
    pub middle_name: Option<String>,
    
    #[serde(skip)]
    pub password_hash: String,
    
    pub email: String,
    pub age: u32,
    
    #[serde(with = "chrono::serde::ts_seconds")]
    pub created_at: DateTime<Utc>,
    
    pub updated_at: DateTime<Utc>,
}
}

常見的 derive 組合

#![allow(unused)]
fn main() {
// 基本資料結構
#[derive(Debug, Clone, PartialEq, Eq)]

// 可序列化的資料
#[derive(Debug, Clone, Serialize, Deserialize)]

// 可雜湊的資料
#[derive(Debug, Clone, PartialEq, Eq, Hash)]

// 可排序的資料
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]

// 具有預設值的資料
#[derive(Debug, Clone, Default)]
}

效能考量

編譯時間

  • derive:編譯時生成程式碼,對編譯時間影響較小
  • 手動實作:需要編譯更多程式碼,編譯時間較長

執行時間

  • derive:生成的程式碼通常是最佳化的
  • 手動實作:可以針對特定需求進行最佳化,但需要專業知識

記憶體使用

  • derive:標準實作,記憶體使用合理
  • 手動實作:可以針對記憶體使用進行最佳化

常見錯誤和解決方案

錯誤 1:遺漏必要的 feature

#![allow(unused)]
fn main() {
// 錯誤:沒有啟用 derive feature
// Cargo.toml 中應該是:
// serde = { version = "1.0", features = ["derive"] }
}

錯誤 2:循環依賴

#![allow(unused)]
fn main() {
// 錯誤:結構體之間的循環引用
#[derive(Debug, Clone)]
struct A {
    b: B,
}

#[derive(Debug, Clone)]
struct B {
    a: A,  // 這會導致編譯錯誤
}

// 解決方案:使用 Box 或 Rc
#[derive(Debug, Clone)]
struct A {
    b: Box<B>,
}

#[derive(Debug, Clone)]
struct B {
    a: Box<A>,
}
}

錯誤 3:泛型參數問題

#![allow(unused)]
fn main() {
// 錯誤:泛型參數沒有適當的約束
#[derive(Debug, Clone)]
struct Container<T> {
    value: T,
}

// 解決方案:添加適當的約束
#[derive(Debug, Clone)]
struct Container<T: Debug + Clone> {
    value: T,
}
}

實用工具和庫

常用的 derive 相關 crate

  • serde:序列化/反序列化
  • clap:命令列參數解析的 derive
  • thiserror:錯誤處理的 derive
  • strum:枚舉相關的 derive

開發工具

  • cargo expand:查看 derive 生成的程式碼
  • rust-analyzer:IDE 支援,可以顯示 derive 的效果

測試和除錯

測試 derive 功能

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_user_debug() {
        let user = User { /* ... */ };
        let debug_str = format!("{:?}", user);
        assert!(debug_str.contains("John Doe"));
    }

    #[test]
    fn test_user_clone() {
        let user = User { /* ... */ };
        let cloned = user.clone();
        assert_eq!(user.id, cloned.id);
    }

    #[test]
    fn test_user_serialization() {
        let user = User { /* ... */ };
        let json = serde_json::to_string(&user).unwrap();
        let deserialized: User = serde_json::from_str(&json).unwrap();
        assert_eq!(user.id, deserialized.id);
    }
}
}

除錯技巧

#![allow(unused)]
fn main() {
// 使用 cargo expand 查看生成的程式碼
// cargo install cargo-expand
// cargo expand --lib

// 使用 dbg! 宏來除錯
let user = dbg!(User { /* ... */ });
}

結論

#[derive(...)] 是 Rust 語言的強大特性,能夠用極少的程式碼實現複雜的功能。在大多數情況下,使用 derive 是最佳選擇,除非有特殊的客製化需求。這個特性不僅提升了開發效率,也減少了錯誤發生的機會,是 Rust 開發者必須掌握的重要工具。

學習建議

  1. 初學者:優先學習和使用 derive 屬性
  2. 進階開發者:了解手動實作的細節,以便在需要時進行客製化
  3. 專案開發:在實際專案中優先使用 derive,只在特殊需求時才手動實作
  4. 效能敏感:在效能關鍵的地方考慮手動實作和最佳化

掌握 #[derive(...)] 的使用,能夠讓你在 Rust 開發中事半功倍,同時保持程式碼的簡潔和可維護性。 |------|-----------|-------------| | 程式碼量 | 1 行 | 需要 100+ 行手動實作 | | 除錯輸出 | println!("{:?}", user) 直接可用 | 編譯錯誤,需要手動實作 Debug | | 複製物件 | user.clone() 直接可用 | 編譯錯誤,需要手動實作 Clone | | JSON 序列化 | serde_json::to_string(&user) 直接可用 | 編譯錯誤,需要手動實作 Serialize | | JSON 反序列化 | serde_json::from_str(&json) 直接可用 | 編譯錯誤,需要手動實作 Deserialize | | 維護成本 | 低 | 高 | | 出錯風險 | 低 | 高 |

結論

#[derive(...)] 讓你用 1 行程式碼就能獲得原本需要數十行手動實作的功能,大幅提升開發效率並減少出錯機會。除非有特殊需求需要客製化實作,否則建議優先使用 derive 屬性。

適用情境

建議使用 derive

  • 一般的資料結構
  • 標準的序列化/反序列化需求
  • 快速開發原型

考慮手動實作

  • 需要客製化的序列化格式
  • 特殊的 Debug 輸出需求
  • 效能敏感的場景需要優化
  • 複雜的欄位驗證邏輯

Rust Traits 白話解釋:與 C++ 的比較

什麼是 Trait?

Trait 就像是一個「能力證書」或「技能清單」

想像一下:

  • 你去考駕照,駕照就是一個 trait,它證明你有「開車」的能力
  • 不管你開的是轎車、卡車還是機車,只要有駕照,就代表你具備了「開車」這個技能
  • 不同的車輛類型都可以「實作」這個「開車」的 trait

基本範例:飛行能力

Rust 版本

// 定義一個 trait(能力清單)
trait CanFly {
    fn fly(&self);
}

// 不同的東西都可以實作這個 trait
struct Bird;
struct Airplane;
struct Superman;

// 鳥類實作飛行能力
impl CanFly for Bird {
    fn fly(&self) {
        println!("用翅膀飛翔!");
    }
}

// 飛機實作飛行能力
impl CanFly for Airplane {
    fn fly(&self) {
        println!("用引擎和機翼飛行!");
    }
}

// 超人實作飛行能力
impl CanFly for Superman {
    fn fly(&self) {
        println!("用超能力飛行!");
    }
}

// 使用這個 trait
fn make_it_fly(thing: &dyn CanFly) {
    thing.fly(); // 不管是什麼東西,只要會飛就行
}

fn main() {
    let bird = Bird;
    let airplane = Airplane;
    let superman = Superman;
    
    make_it_fly(&bird);      // 用翅膀飛翔!
    make_it_fly(&airplane);  // 用引擎和機翼飛行!
    make_it_fly(&superman);  // 用超能力飛行!
}

C++ 對應版本

#include <iostream>
#include <memory>

// C++ 版本 - 抽象類別(相當於 trait)
class CanFly {
public:
    virtual void fly() = 0;  // 純虛函數
    virtual ~CanFly() = default;
};

class Bird : public CanFly {
public:
    void fly() override {
        std::cout << "用翅膀飛翔!" << std::endl;
    }
};

class Airplane : public CanFly {
public:
    void fly() override {
        std::cout << "用引擎和機翼飛行!" << std::endl;
    }
};

class Superman : public CanFly {
public:
    void fly() override {
        std::cout << "用超能力飛行!" << std::endl;
    }
};

// 使用多型
void makeItFly(CanFly* thing) {
    thing->fly();
}

int main() {
    auto bird = std::make_unique<Bird>();
    auto airplane = std::make_unique<Airplane>();
    auto superman = std::make_unique<Superman>();
    
    makeItFly(bird.get());      // 用翅膀飛翔!
    makeItFly(airplane.get());  // 用引擎和機翼飛行!
    makeItFly(superman.get());  // 用超能力飛行!
    
    return 0;
}

Rust 與 C++ 的對應關係

RustC++說明
traitinterface (抽象類別)定義一組方法簽章
impl Trait for Typeclass Type : public Interface實作介面
&dyn TraitInterface*多型的使用方式
impl 區塊override 方法實作具體功能

主要差異

1. 語法差異

  • Rust: trait + impl 分離定義
  • C++: class + 繼承一起定義

2. 實作方式

Rust 可以為已存在的型別「後加」trait

// Rust 可以這樣做
impl CanFly for i32 {  // 為內建型別 i32 加上飛行能力
    fn fly(&self) {
        println!("數字 {} 神奇地飛起來了!", self);
    }
}

fn main() {
    let number = 42;
    number.fly(); // 數字 42 神奇地飛起來了!
}

C++ 不能為內建型別後加介面

// C++ 不能這樣做
// 你不能為內建的 int 類型後加介面
// int 必須在定義時就決定要繼承哪些類別

3. 記憶體管理

  • Rust: &dyn Trait 是借用,自動管理記憶體
  • C++: Interface* 需要手動管理記憶體或使用智慧指標

4. 多重能力

Rust 支援多個 trait

#![allow(unused)]
fn main() {
trait CanWalk {
    fn walk(&self);
}

trait CanSwim {
    fn swim(&self);
}

// 可以同時實作多個 trait
impl CanWalk for Duck { /* ... */ }
impl CanSwim for Duck { /* ... */ }
impl CanFly for Duck { /* ... */ }
}

C++ 多重繼承

class Duck : public CanWalk, public CanSwim, public CanFly {
    // 需要實作所有純虛函數
};

實際例子:動物園管理系統

Rust 版本

// 定義各種能力
trait CanWalk {
    fn walk(&self);
    fn walk_speed(&self) -> u32;
}

trait CanSwim {
    fn swim(&self);
    fn swim_speed(&self) -> u32;
}

trait CanFly {
    fn fly(&self);
    fn fly_speed(&self) -> u32;
}

// 鴨子:會走、會游、會飛
struct Duck {
    name: String,
}

impl CanWalk for Duck {
    fn walk(&self) {
        println!("鴨子 {} 用腳走路", self.name);
    }
    
    fn walk_speed(&self) -> u32 { 5 }
}

impl CanSwim for Duck {
    fn swim(&self) {
        println!("鴨子 {} 在水中游泳", self.name);
    }
    
    fn swim_speed(&self) -> u32 { 10 }
}

impl CanFly for Duck {
    fn fly(&self) {
        println!("鴨子 {} 拍翅膀飛行", self.name);
    }
    
    fn fly_speed(&self) -> u32 { 20 }
}

// 魚:只會游泳
struct Fish {
    name: String,
}

impl CanSwim for Fish {
    fn swim(&self) {
        println!("魚 {} 在水中游泳", self.name);
    }
    
    fn swim_speed(&self) -> u32 { 15 }
}

// 鳥:會走和飛
struct Eagle {
    name: String,
}

impl CanWalk for Eagle {
    fn walk(&self) {
        println!("老鷹 {} 用爪子走路", self.name);
    }
    
    fn walk_speed(&self) -> u32 { 3 }
}

impl CanFly for Eagle {
    fn fly(&self) {
        println!("老鷹 {} 展翅翱翔", self.name);
    }
    
    fn fly_speed(&self) -> u32 { 50 }
}

// 使用函數
fn swimming_competition(swimmers: Vec<&dyn CanSwim>) {
    println!("=== 游泳比賽開始 ===");
    for swimmer in swimmers {
        swimmer.swim();
        println!("速度: {} km/h", swimmer.swim_speed());
    }
}

fn flying_show(flyers: Vec<&dyn CanFly>) {
    println!("=== 飛行表演開始 ===");
    for flyer in flyers {
        flyer.fly();
        println!("速度: {} km/h", flyer.fly_speed());
    }
}

fn walking_parade(walkers: Vec<&dyn CanWalk>) {
    println!("=== 行走遊行開始 ===");
    for walker in walkers {
        walker.walk();
        println!("速度: {} km/h", walker.walk_speed());
    }
}

fn main() {
    let duck = Duck { name: "唐老鴨".to_string() };
    let fish = Fish { name: "尼莫".to_string() };
    let eagle = Eagle { name: "老鷹".to_string() };
    
    // 游泳比賽:鴨子和魚都可以參加
    swimming_competition(vec![&duck, &fish]);
    println!();
    
    // 飛行表演:鴨子和老鷹都可以參加
    flying_show(vec![&duck, &eagle]);
    println!();
    
    // 行走遊行:鴨子和老鷹都可以參加
    walking_parade(vec![&duck, &eagle]);
}

C++ 對應版本

#include <iostream>
#include <vector>
#include <memory>
#include <string>

// 抽象介面
class CanWalk {
public:
    virtual void walk() = 0;
    virtual int walk_speed() = 0;
    virtual ~CanWalk() = default;
};

class CanSwim {
public:
    virtual void swim() = 0;
    virtual int swim_speed() = 0;
    virtual ~CanSwim() = default;
};

class CanFly {
public:
    virtual void fly() = 0;
    virtual int fly_speed() = 0;
    virtual ~CanFly() = default;
};

// 鴨子:多重繼承
class Duck : public CanWalk, public CanSwim, public CanFly {
private:
    std::string name;
    
public:
    Duck(const std::string& n) : name(n) {}
    
    void walk() override {
        std::cout << "鴨子 " << name << " 用腳走路" << std::endl;
    }
    
    int walk_speed() override { return 5; }
    
    void swim() override {
        std::cout << "鴨子 " << name << " 在水中游泳" << std::endl;
    }
    
    int swim_speed() override { return 10; }
    
    void fly() override {
        std::cout << "鴨子 " << name << " 拍翅膀飛行" << std::endl;
    }
    
    int fly_speed() override { return 20; }
};

// 魚:只繼承游泳
class Fish : public CanSwim {
private:
    std::string name;
    
public:
    Fish(const std::string& n) : name(n) {}
    
    void swim() override {
        std::cout << "魚 " << name << " 在水中游泳" << std::endl;
    }
    
    int swim_speed() override { return 15; }
};

// 老鷹:繼承走路和飛行
class Eagle : public CanWalk, public CanFly {
private:
    std::string name;
    
public:
    Eagle(const std::string& n) : name(n) {}
    
    void walk() override {
        std::cout << "老鷹 " << name << " 用爪子走路" << std::endl;
    }
    
    int walk_speed() override { return 3; }
    
    void fly() override {
        std::cout << "老鷹 " << name << " 展翅翱翔" << std::endl;
    }
    
    int fly_speed() override { return 50; }
};

// 使用函數
void swimming_competition(const std::vector<CanSwim*>& swimmers) {
    std::cout << "=== 游泳比賽開始 ===" << std::endl;
    for (auto swimmer : swimmers) {
        swimmer->swim();
        std::cout << "速度: " << swimmer->swim_speed() << " km/h" << std::endl;
    }
}

void flying_show(const std::vector<CanFly*>& flyers) {
    std::cout << "=== 飛行表演開始 ===" << std::endl;
    for (auto flyer : flyers) {
        flyer->fly();
        std::cout << "速度: " << flyer->fly_speed() << " km/h" << std::endl;
    }
}

void walking_parade(const std::vector<CanWalk*>& walkers) {
    std::cout << "=== 行走遊行開始 ===" << std::endl;
    for (auto walker : walkers) {
        walker->walk();
        std::cout << "速度: " << walker->walk_speed() << " km/h" << std::endl;
    }
}

int main() {
    auto duck = std::make_unique<Duck>("唐老鴨");
    auto fish = std::make_unique<Fish>("尼莫");
    auto eagle = std::make_unique<Eagle>("老鷹");
    
    // 游泳比賽
    swimming_competition({duck.get(), fish.get()});
    std::cout << std::endl;
    
    // 飛行表演
    flying_show({duck.get(), eagle.get()});
    std::cout << std::endl;
    
    // 行走遊行
    walking_parade({duck.get(), eagle.get()});
    
    return 0;
}

常用的 Rust Traits

標準庫 Traits

#![allow(unused)]
fn main() {
// Debug - 用於 {:?} 格式化
#[derive(Debug)]
struct Point { x: i32, y: i32 }

// Clone - 用於 .clone() 方法
#[derive(Clone)]
struct Data { value: String }

// PartialEq - 用於 == 比較
#[derive(PartialEq)]
struct Id(u32);

// Display - 用於 {} 格式化
use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}
}

自訂 Traits

#![allow(unused)]
fn main() {
// 定義計算面積的能力
trait Area {
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Area for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

impl Area for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
}

// 使用 trait
fn print_area<T: Area>(shape: &T) {
    println!("面積: {}", shape.area());
}
}

進階特性

Trait Bounds(特徵約束)

#![allow(unused)]
fn main() {
// 限制泛型必須實作特定 trait
fn compare_and_print<T: PartialEq + Debug>(a: &T, b: &T) {
    if a == b {
        println!("{:?} 等於 {:?}", a, b);
    } else {
        println!("{:?} 不等於 {:?}", a, b);
    }
}

// 或者使用 where 子句
fn process_data<T>(data: &T) 
where 
    T: Clone + Debug + PartialEq,
{
    let cloned = data.clone();
    println!("原始: {:?}", data);
    println!("複製: {:?}", cloned);
    println!("相等: {}", data == &cloned);
}
}

預設實作

#![allow(unused)]
fn main() {
trait Greet {
    fn name(&self) -> &str;
    
    // 預設實作
    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
    
    // 可以被覆寫的預設實作
    fn goodbye(&self) {
        println!("Goodbye, {}!", self.name());
    }
}

struct Person {
    name: String,
}

impl Greet for Person {
    fn name(&self) -> &str {
        &self.name
    }
    
    // 可以選擇覆寫預設實作
    fn goodbye(&self) {
        println!("See you later, {}!", self.name());
    }
}
}

總結

Rust Trait 的核心概念

  • 像是技能證書:定義「能做什麼」
  • 可以後加:為已存在的型別加上新能力
  • 組合式設計:一個型別可以有多種能力
  • 類型安全:編譯時就確保型別有對應的能力

與 C++ 的主要差異

  • Rust 更靈活:可以為任何型別後加 trait
  • C++ 更傳統:繼承式設計,必須在定義時決定
  • Rust 記憶體更安全:自動管理記憶體,無需手動釋放
  • C++ 效能更直接:虛函數表調用,但需要小心記憶體管理

使用建議

  1. 優先使用 derive:對於常用的 trait,儘量使用 #[derive(...)]
  2. 組合勝於繼承:使用多個小 trait 組合,而不是大的 trait
  3. 為外部型別添加能力:利用 trait 的靈活性為第三方型別加功能
  4. 善用 trait bounds:在泛型中使用 trait 約束來確保型別安全

Trait 是 Rust 的核心特性,讓程式碼更模組化、更易於維護,同時保持高效能和型別安全!

Rust 中 traitimpl 的差異

trait - 定義能力清單

trait 用來定義一組方法的簽名,像是「合約」或「介面」。

#![allow(unused)]
fn main() {
trait CanFly {
    fn fly(&self);        // 只定義方法簽名
    fn land(&self);       // 沒有具體實作
}
}

特點

  • 定義:宣告一組方法的簽名
  • 用途:像是「合約」或「介面」,規定什麼能力必須實作
  • 內容:通常只有方法簽名,不包含具體實作

impl - 具體實作

impl 用來為特定的結構體實作 trait 的方法。

#![allow(unused)]
fn main() {
impl CanFly for Bird {
    fn fly(&self) {
        println!("用翅膀飛翔!");  // 具體的實作內容
    }
    fn land(&self) {
        println!("降落在樹上!");
    }
}
}

特點

  • 實作:為特定的結構體實作 trait 的方法
  • 用途:提供具體的功能代碼
  • 內容:包含完整的方法實作

簡單比喻

概念比喻說明
trait「會飛的能力清單」規定必須有 fly() 方法
impl「具體怎麼飛」鳥用翅膀飛、飛機用引擎飛

完整範例

// trait:定義能力
trait CanSwim {
    fn swim(&self);
}

// 不同的結構體
struct Fish;
struct Bird;

// impl:為 Fish 實作游泳能力
impl CanSwim for Fish {
    fn swim(&self) {
        println!("魚類用鰭游泳!");
    }
}

// impl:為 Bird 實作游泳能力
impl CanSwim for Bird {
    fn swim(&self) {
        println!("鳥類划水游泳!");
    }
}

fn main() {
    let fish = Fish;
    let bird = Bird;
    
    fish.swim();  // 魚類用鰭游泳!
    bird.swim();  // 鳥類划水游泳!
}

重點總結

trait 定義「要做什麼」,impl 定義「怎麼做」!

  • trait 就像是規格書,告訴你需要實作哪些功能
  • impl 就像是實際的程式碼,告訴你這些功能具體如何運作

這樣的設計讓不同的類型可以用不同的方式實作相同的功能,提供了很大的靈活性。

Rust vs C++ 詳細對比指南

前言

Rust 和 C++ 都是系統級程式語言,但設計哲學截然不同。C++ 給你完全的控制權,但需要你自己管理記憶體;Rust 則通過編譯時檢查來確保記憶體安全。


第1篇 Rust基礎知識

第1章 Rust入門

1.1 Rust簡介

白話解釋:

  • C++: 像是一把瑞士刀,功能強大但容易割傷自己
  • Rust: 像是一把智能刀,有安全鎖,不讓你割傷自己,但學會使用需要時間

設計理念對比:

  • C++: "信任程序員,給他們所有控制權"
  • Rust: "幫助程序員寫出安全的程式碼"

1.2 第1個程式

C++ Hello World:

#include <iostream>
using namespace std;

int main() {
    cout << "Hello, World!" << endl;
    return 0;
}

Rust Hello World:

fn main() {
    println!("Hello, World!");
}

差異說明:

  • C++ 需要 #includereturn 0
  • Rust 更簡潔,println! 是宏(macro)
  • Rust 不需要明確返回值

1.3 Rust基礎語法

1.3.1 註釋與打印文本

C++:

// 單行註釋
/* 多行註釋 */
cout << "Hello" << endl;
printf("格式化: %d", 42);

Rust:

#![allow(unused)]
fn main() {
// 單行註釋
/* 多行註釋 */
println!("Hello");
println!("格式化: {}", 42);
}

白話解釋:

  • Rust 的 println! 更安全,會在編譯時檢查格式
  • C++ 的 printf 在運行時才檢查,容易出錯
1.3.2 變量和變量可變性

C++:

int x = 5;          // 可變
const int y = 10;   // 不可變
x = 6;              // OK
// y = 11;          // 編譯錯誤

Rust:

#![allow(unused)]
fn main() {
let x = 5;          // 不可變(預設)
let mut y = 10;     // 可變
let z = 15;         // 不可變
// x = 6;           // 編譯錯誤
y = 11;             // OK
}

白話解釋:

  • C++: 變數預設可變,要不可變需要加 const
  • Rust: 變數預設不可變,要可變需要加 mut
  • 這個設計讓程式更安全,因為大部分時候我們不需要改變變數
1.3.3 常量

C++:

const int MAX_SIZE = 100;
#define PI 3.14159

Rust:

#![allow(unused)]
fn main() {
const MAX_SIZE: i32 = 100;
const PI: f64 = 3.14159;
}

差異:

  • Rust 的常量必須指定類型
  • Rust 沒有 #define,所有常量都是類型安全的
1.3.4 運算符

基本運算符兩者相似,但有一些差異:

C++:

int a = 5, b = 2;
int result = a / b;  // 整數除法 = 2

Rust:

#![allow(unused)]
fn main() {
let a = 5;
let b = 2;
let result = a / b;  // 整數除法 = 2
// let mixed = a / 2.0;  // 編譯錯誤!類型不匹配
}

白話解釋:

  • Rust 不允許不同類型直接運算,需要明確轉換
  • 這避免了意外的類型轉換錯誤
1.3.5 流程控制語句

C++ if 語句:

int x = 5;
if (x > 0) {
    cout << "正數" << endl;
} else if (x < 0) {
    cout << "負數" << endl;
} else {
    cout << "零" << endl;
}

Rust if 語句:

#![allow(unused)]
fn main() {
let x = 5;
if x > 0 {
    println!("正數");
} else if x < 0 {
    println!("負數");
} else {
    println!("零");
}
}

Rust 的 if 是表達式:

#![allow(unused)]
fn main() {
let x = 5;
let description = if x > 0 { "正數" } else { "非正數" };
}

迴圈比較:

C++:

// for 迴圈
for (int i = 0; i < 5; i++) {
    cout << i << endl;
}

// while 迴圈
int i = 0;
while (i < 5) {
    cout << i << endl;
    i++;
}

Rust:

#![allow(unused)]
fn main() {
// for 迴圈
for i in 0..5 {
    println!("{}", i);
}

// while 迴圈
let mut i = 0;
while i < 5 {
    println!("{}", i);
    i += 1;
}

// loop 迴圈(無限迴圈)
let mut count = 0;
loop {
    println!("{}", count);
    count += 1;
    if count >= 5 {
        break;
    }
}
}

1.4 Rust數據類型

1.4.1 標量類型

C++:

int a = 42;
float b = 3.14f;
double c = 3.14159;
char d = 'A';
bool e = true;

Rust:

#![allow(unused)]
fn main() {
let a: i32 = 42;        // 32位整數
let b: f32 = 3.14;      // 32位浮點數
let c: f64 = 3.14159;   // 64位浮點數
let d: char = 'A';      // Unicode字符
let e: bool = true;     // 布林值
}

白話解釋:

  • Rust 的整數類型更明確:i8, i16, i32, i64, i128
  • Rust 的 char 是 4 位元組,支援所有 Unicode 字符
  • C++ 的 char 只有 1 位元組
1.4.2 複合數據類型

陣列比較:

C++:

int arr[5] = {1, 2, 3, 4, 5};
int arr2[] = {1, 2, 3};  // 大小自動推斷

Rust:

#![allow(unused)]
fn main() {
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let arr2 = [1, 2, 3];  // 類型推斷為 [i32; 3]
let arr3 = [0; 5];     // [0, 0, 0, 0, 0]
}

元組比較:

C++ (C++11後):

#include <tuple>
std::tuple<int, double, char> tup = std::make_tuple(1, 2.5, 'A');
auto [x, y, z] = tup;  // C++17 結構化綁定

Rust:

#![allow(unused)]
fn main() {
let tup: (i32, f64, char) = (1, 2.5, 'A');
let (x, y, z) = tup;  // 解構
let first = tup.0;    // 通過索引訪問
}
1.4.3 字符串

這是 Rust 和 C++ 最大的差異之一!

C++:

#include <string>
std::string s1 = "Hello";
char s2[] = "World";
const char* s3 = "C++";

Rust:

#![allow(unused)]
fn main() {
let s1 = "Hello";           // &str (字符串切片)
let s2 = String::from("World"); // String (擁有所有權)
let s3 = "Rust".to_string();    // 另一種創建String的方式
}

白話解釋:

  • C++: 字符串類型複雜,容易混淆
  • Rust:
    • &str 是借用的字符串切片(類似C++的const char*
    • String 是擁有所有權的字符串(類似C++的std::string

1.5 函數與閉包

1.5.1 函數

C++:

int add(int a, int b) {
    return a + b;
}

void greet(const std::string& name) {
    std::cout << "Hello, " << name << std::endl;
}

Rust:

#![allow(unused)]
fn main() {
fn add(a: i32, b: i32) -> i32 {
    a + b  // 最後一個表達式是返回值
}

fn greet(name: &str) {
    println!("Hello, {}", name);
}
}

白話解釋:

  • Rust 函數最後一個表達式自動成為返回值
  • 如果有分號,就不是返回值了
1.5.2 閉包

C++ (C++11後):

auto add = [](int a, int b) -> int {
    return a + b;
};

int x = 10;
auto add_x = [x](int a) -> int {
    return a + x;
};

Rust:

#![allow(unused)]
fn main() {
let add = |a: i32, b: i32| -> i32 {
    a + b
};

let x = 10;
let add_x = |a| a + x;  // 類型推斷
}

白話解釋:

  • Rust 的閉包語法更簡潔
  • Rust 的閉包會自動捕獲環境變量

1.6 類型系統

1.6.1 泛型

C++:

template<typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

Rust:

#![allow(unused)]
fn main() {
fn max<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}
}

白話解釋:

  • C++ 的模板在實例化時才檢查約束
  • Rust 的泛型在定義時就檢查約束(T: PartialOrd
1.6.2 trait

C++(使用介面):

class Drawable {
public:
    virtual void draw() = 0;
};

class Circle : public Drawable {
public:
    void draw() override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

Rust:

#![allow(unused)]
fn main() {
trait Drawable {
    fn draw(&self);
}

struct Circle;

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing a circle");
    }
}
}

白話解釋:

  • C++ 用繼承實現多態
  • Rust 用 trait 實現多態,更靈活
1.6.3 類型轉換

C++:

int x = 42;
double y = static_cast<double>(x);  // 顯式轉換
double z = x;  // 隱式轉換

Rust:

#![allow(unused)]
fn main() {
let x = 42i32;
let y = x as f64;  // 顯式轉換
// let z: f64 = x;  // 編譯錯誤!無隱式轉換
}

白話解釋:

  • Rust 不允許隱式類型轉換,避免意外錯誤
  • 所有轉換都必須明確

第2章 Rust基礎

2.1 所有權系統

這是 Rust 最獨特的特性!

2.1.1 所有權機制

C++(手動管理):

void example() {
    int* ptr = new int(42);
    // 使用 ptr
    delete ptr;  // 必須手動釋放
}

Rust(自動管理):

#![allow(unused)]
fn main() {
fn example() {
    let data = Box::new(42);
    // 使用 data
    // 自動釋放,無需手動管理
}
}

白話解釋:

  • C++: "你負責清理你創建的東西"
  • Rust: "我幫你自動清理,你不用擔心"
2.1.2 引用和借用

C++:

void function(int& ref) {  // 引用
    ref = 42;
}

void function2(const int& ref) {  // 常量引用
    // ref = 42;  // 錯誤
}

Rust:

#![allow(unused)]
fn main() {
fn function(r: &mut i32) {  // 可變借用
    *r = 42;
}

fn function2(r: &i32) {  // 不可變借用
    // *r = 42;  // 錯誤
}
}

借用規則(Rust獨有):

#![allow(unused)]
fn main() {
let mut x = 5;
let r1 = &x;        // 不可變借用
let r2 = &x;        // 可以有多個不可變借用
// let r3 = &mut x; // 錯誤!不能同時有可變和不可變借用
}

白話解釋:

  • Rust 的借用檢查器防止數據競爭
  • 同一時間只能有一個可變借用,或多個不可變借用
2.1.3 生命周期

C++(常見錯誤):

int* dangerous_function() {
    int local = 42;
    return &local;  // 錯誤!返回局部變量的引用
}

Rust(編譯時防止):

#![allow(unused)]
fn main() {
fn dangerous_function() -> &i32 {
    let local = 42;
    &local  // 編譯錯誤!生命周期不匹配
}
}

正確的生命周期:

#![allow(unused)]
fn main() {
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}
}

2.2 宏

C++:

#define MAX(a, b) ((a) > (b) ? (a) : (b))  // 不安全

Rust:

#![allow(unused)]
fn main() {
macro_rules! max {
    ($a:expr, $b:expr) => {
        if $a > $b { $a } else { $b }
    };
}
}

白話解釋:

  • C++ 的宏是簡單的文本替換
  • Rust 的宏是語法感知的,更安全

2.3 智能指針

2.3.1 什麼是智能指針

白話解釋:

  • 普通指針:就像房子的鑰匙,但你得記住鎖門
  • 智能指針:像自動鎖門的鑰匙,會幫你管理
2.3.2 Box

C++:

std::unique_ptr<int> ptr = std::make_unique<int>(42);

Rust:

#![allow(unused)]
fn main() {
let ptr = Box::new(42);
}
2.3.3 Rc(Reference Counting)

C++:

std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1;  // 引用計數 +1

Rust:

#![allow(unused)]
fn main() {
use std::rc::Rc;
let ptr1 = Rc::new(42);
let ptr2 = Rc::clone(&ptr1);  // 引用計數 +1
}
2.3.4 RefCell

白話解釋:

  • 允許在不可變借用中進行可變操作
  • 在運行時檢查借用規則,而不是編譯時
#![allow(unused)]
fn main() {
use std::cell::RefCell;
let data = RefCell::new(42);
let mut_ref = data.borrow_mut();
*mut_ref = 100;
}

2.4 多線程

2.4.1 什麼是多線程

白話解釋:

  • 單線程:一個人做所有事情
  • 多線程:多個人同時做不同事情
2.4.2 創建線程

C++:

#include <thread>
std::thread t([]() {
    std::cout << "Hello from thread" << std::endl;
});
t.join();

Rust:

#![allow(unused)]
fn main() {
use std::thread;
let handle = thread::spawn(|| {
    println!("Hello from thread");
});
handle.join().unwrap();
}
2.4.3 線程間的數據共享

C++(需要手動同步):

#include <mutex>
std::mutex mtx;
int shared_data = 0;

void increment() {
    std::lock_guard<std::mutex> lock(mtx);
    shared_data++;
}

Rust(編譯時保證安全):

#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};
let shared_data = Arc::new(Mutex::new(0));
let data_clone = shared_data.clone();

thread::spawn(move || {
    let mut data = data_clone.lock().unwrap();
    *data += 1;
});
}

2.5 錯誤處理

2.5.1 可恢復錯誤

C++:

#include <optional>
std::optional<int> divide(int a, int b) {
    if (b == 0) {
        return std::nullopt;
    }
    return a / b;
}

Rust:

#![allow(unused)]
fn main() {
fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err("Division by zero".to_string())
    } else {
        Ok(a / b)
    }
}

// 使用
match divide(10, 2) {
    Ok(result) => println!("Result: {}", result),
    Err(error) => println!("Error: {}", error),
}
}
2.5.2 不可恢復錯誤

C++:

#include <stdexcept>
throw std::runtime_error("Something went wrong");

Rust:

#![allow(unused)]
fn main() {
panic!("Something went wrong");
}

2.6 包和crate

白話解釋:

  • C++: 使用 #include 和鏈接器
  • Rust: 使用 Cargo.toml 管理依賴

Cargo.toml:

[package]
name = "my_project"
version = "0.1.0"

[dependencies]
serde = "1.0"

2.7 模塊

C++:

// math.h
namespace math {
    int add(int a, int b);
}

// math.cpp
#include "math.h"
int math::add(int a, int b) {
    return a + b;
}

Rust:

#![allow(unused)]
fn main() {
// lib.rs
mod math {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }
}

// 使用
use math::add;
}

2.8 單元測試

C++(需要外部框架):

// 使用 Google Test
TEST(MathTest, Addition) {
    EXPECT_EQ(add(2, 3), 5);
}

Rust(內建支援):

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_addition() {
        assert_eq!(add(2, 3), 5);
    }
}
}

2.9 調試

C++:

#include <iostream>
std::cout << "Debug: x = " << x << std::endl;

Rust:

#![allow(unused)]
fn main() {
println!("Debug: x = {:?}", x);
// 或者使用 debug 宏
dbg!(x);
}


Rust 拿掉了 C++ 的什麼?為什麼?

1. 拿掉了手動記憶體管理

C++ 的問題:

void memory_leak_example() {
    int* ptr = new int(42);
    // 忘記 delete ptr; 
    // 記憶體洩漏!
}

void dangling_pointer_example() {
    int* ptr;
    {
        int local = 42;
        ptr = &local;
    }
    // ptr 現在指向無效記憶體!
    cout << *ptr;  // 未定義行為
}

Rust 的解決方案:

#![allow(unused)]
fn main() {
fn safe_memory_example() {
    let data = Box::new(42);
    // 自動清理,不會洩漏
}

fn no_dangling_pointer() {
    let ptr;
    {
        let local = 42;
        // ptr = &local;  // 編譯錯誤!
    }
    // Rust 不允許懸空指標
}
}

白話解釋:

  • 問題:C++ 像是給你一把槍但沒有保險,你可能會意外射到自己
  • 解決:Rust 像是智能槍,有多重安全機制,防止意外傷害
  • 好處:99% 的記憶體相關 bug 在編譯時就被抓到了

2. 拿掉了 NULL 指標

C++ 的問題:

int* ptr = nullptr;
*ptr = 42;  // 程式崩潰!

Rust 的解決方案:

#![allow(unused)]
fn main() {
let value: Option<i32> = None;
match value {
    Some(v) => println!("值是: {}", v),
    None => println!("沒有值"),
}
// 強制你處理"沒有值"的情況
}

白話解釋:

  • 問題:NULL 指標像是不存在的地址,去那裡會迷路
  • 解決:Rust 用 Option<T> 明確表示"可能沒有值"
  • 好處:編譯器強制你考慮所有情況,避免意外崩潰

3. 拿掉了資料競爭

C++ 的問題:

int counter = 0;

void thread1() { counter++; }
void thread2() { counter++; }

// 兩個執行緒同時修改 counter,結果不可預測

Rust 的解決方案:

#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};

let counter = Arc::new(Mutex::new(0));
let counter1 = counter.clone();
let counter2 = counter.clone();

thread::spawn(move || {
    let mut num = counter1.lock().unwrap();
    *num += 1;
});

thread::spawn(move || {
    let mut num = counter2.lock().unwrap();
    *num += 1;
});
}

白話解釋:

  • 問題:多執行緒像是多人同時編輯同一文件,會亂掉
  • 解決:Rust 強制使用鎖或其他同步機制
  • 好處:編譯時就防止資料競爭,不會有神秘的併發 bug

4. 拿掉了未初始化變數

C++ 的問題:

int x;  // 未初始化
cout << x;  // 印出垃圾值

Rust 的解決方案:

#![allow(unused)]
fn main() {
let x: i32;  // 聲明但未初始化
// println!("{}", x);  // 編譯錯誤!
x = 42;  // 必須先初始化
println!("{}", x);  // 現在可以用了
}

白話解釋:

  • 問題:未初始化變數像是空的盒子,不知道裡面裝什麼
  • 解決:Rust 不允許使用未初始化的變數
  • 好處:避免讀取到隨機值導致的 bug

5. 拿掉了隱式類型轉換

C++ 的問題:

int a = 10;
double b = 3.14;
int result = a + b;  // 隱式轉換,可能失去精度

Rust 的解決方案:

#![allow(unused)]
fn main() {
let a = 10i32;
let b = 3.14f64;
// let result = a + b;  // 編譯錯誤!
let result = a as f64 + b;  // 必須明確轉換
}

白話解釋:

  • 問題:隱式轉換像是自動翻譯,有時會翻錯意思
  • 解決:Rust 要求所有轉換都要明確
  • 好處:避免意外的精度丟失或類型錯誤

6. 拿掉了繼承

C++ 的複雜繼承:

class A { public: virtual void foo() = 0; };
class B { public: virtual void bar() = 0; };
class C : public A, public B {  // 多重繼承
    // 複雜的菱形繼承問題...
};

Rust 的組合方式:

#![allow(unused)]
fn main() {
trait Foo {
    fn foo(&self);
}

trait Bar {
    fn bar(&self);
}

struct C;

impl Foo for C {
    fn foo(&self) { println!("foo"); }
}

impl Bar for C {
    fn bar(&self) { println!("bar"); }
}
}

白話解釋:

  • 問題:繼承像是複雜的家族關係,容易搞混
  • 解決:Rust 用組合和 trait,更清晰
  • 好處:避免繼承帶來的複雜性和菱形問題

Rust 特有的功能

1. 所有權系統(Ownership)

這是 Rust 最獨特的特性!

fn take_ownership(s: String) {
    println!("{}", s);
}  // s 在這裡被丟棄

fn main() {
    let s = String::from("hello");
    take_ownership(s);
    // println!("{}", s);  // 編譯錯誤!s 已被移動
}

白話解釋:

  • 就像實體物品,同一時間只能有一個人擁有
  • 當你把東西給別人,你就不再擁有它了
  • 好處:自動記憶體管理,無需垃圾回收器

2. 借用檢查器(Borrow Checker)

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;        // 不可變借用
    let r2 = &s;        // 可以有多個不可變借用
    // let r3 = &mut s; // 錯誤!不能同時有可變和不可變借用
    
    println!("{} and {}", r1, r2);
    // r1 和 r2 不再使用
    
    let r3 = &mut s;    // 現在可以可變借用了
    println!("{}", r3);
}

白話解釋:

  • 就像圖書館借書規則:
    • 可以多人同時「讀」同一本書
    • 但如果有人要「寫筆記」,就只能一個人用
  • 好處:編譯時防止資料競爭

3. 模式匹配(Pattern Matching)

#![allow(unused)]
fn main() {
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn process_message(msg: Message) {
    match msg {
        Message::Quit => println!("退出"),
        Message::Move { x, y } => println!("移動到 ({}, {})", x, y),
        Message::Write(text) => println!("寫入: {}", text),
        Message::ChangeColor(r, g, b) => println!("顏色: ({}, {}, {})", r, g, b),
    }
}
}

白話解釋:

  • 像是超強的 switch,可以拆解複雜的數據結構
  • 編譯器確保你處理了所有可能的情況
  • 好處:安全、強大、表達力強

4. 零成本抽象(Zero-Cost Abstractions)

#![allow(unused)]
fn main() {
// 高階寫法
let numbers: Vec<i32> = vec![1, 2, 3, 4, 5];
let sum: i32 = numbers
    .iter()
    .filter(|&x| x % 2 == 0)
    .map(|&x| x * 2)
    .sum();

// 編譯後等同於手寫的迴圈,沒有額外開銷
}

白話解釋:

  • 像是豪華汽車的自動檔,使用方便但不影響性能
  • 高階抽象在編譯時被優化成低階代碼
  • 好處:寫得爽,跑得快

5. 強大的類型推斷

#![allow(unused)]
fn main() {
let numbers = vec![1, 2, 3];  // 編譯器知道這是 Vec<i32>
let result = numbers.iter().sum();  // 知道這是 i32

// 複雜的情況也能推斷
let data: HashMap<_, _> = vec![("a", 1), ("b", 2)].into_iter().collect();
}

白話解釋:

  • 編譯器像是聰明的助手,能猜出你的意思
  • 你不用寫一堆類型註釋
  • 好處:程式碼簡潔但類型安全

6. 宏系統(Macro System)

#![allow(unused)]
fn main() {
macro_rules! say_hello {
    () => {
        println!("Hello!");
    };
    ($name:expr) => {
        println!("Hello, {}!", $name);
    };
}

say_hello!();           // Hello!
say_hello!("Rust");     // Hello, Rust!
}

白話解釋:

  • 像是程式碼的模板,可以生成重複的代碼
  • 比 C++ 的 #define 更安全更強大
  • 好處:減少重複代碼,類型安全

Rust 的整體好處

1. 記憶體安全 + 性能

白話解釋:

  • 以前你只能選擇:要嘛安全但慢(如 Java),要嘛快但危險(如 C++)
  • Rust 讓你兩個都要:既安全又快
  • 比喻:像是既安全又跑得快的賽車

2. 併發安全

白話解釋:

  • 多執行緒程式設計不再是「祈禱不要出錯」
  • 編譯器幫你檢查,確保執行緒安全
  • 比喻:像是有安全網的走鋼絲

3. 現代化的工具鏈

Cargo(包管理器):

cargo new my_project    # 創建新專案
cargo build            # 編譯
cargo test             # 測試
cargo run              # 執行

白話解釋:

  • 一個工具搞定所有事情
  • 不像 C++ 需要學一堆不同的工具
  • 比喻:像是瑞士刀,功能齊全

4. 優秀的錯誤訊息

Rust 的錯誤訊息:

error[E0382]: borrow of moved value: `s`
  --> src/main.rs:5:20
   |
3  |     let s = String::from("hello");
   |         - move occurs because `s` has type `String`
4  |     take_ownership(s);
   |                    - value moved here
5  |     println!("{}", s);
   |                    ^ value borrowed here after move
   |
   = note: this error occurs because `String` does not implement the `Copy` trait

白話解釋:

  • 不只告訴你錯了,還教你怎麼修
  • 像是有耐心的老師,不只說「錯」,還解釋為什麼錯
  • 好處:學習過程更順暢

5. 向前相容性

白話解釋:

  • Rust 承諾向前相容:今天能編譯的程式,未來也能編譯
  • 不像某些語言會突然改變,讓舊程式無法編譯
  • 好處:投資在 Rust 上比較安全

6. 跨平台

#![allow(unused)]
fn main() {
// 同樣的程式碼可以跨平台編譯
cargo build --target x86_64-pc-windows-gnu     # Windows
cargo build --target x86_64-apple-darwin       # macOS  
cargo build --target x86_64-unknown-linux-gnu  # Linux
}

白話解釋:

  • 一次寫,到處跑
  • 不用為每個平台重寫程式
  • 好處:省時省力

總結:為什麼選擇 Rust?

簡單來說:

  1. 如果你想要 C++ 的速度,但不想被記憶體問題折磨 → 選 Rust
  2. 如果你想寫併發程式,但不想半夜被叫起來修 bug → 選 Rust
  3. 如果你想要現代化的開發體驗 → 選 Rust
  4. 如果你的專案需要長期維護 → 選 Rust

用一句話總結:

Rust 是「如果重新設計 C++,考慮到過去 30 年的經驗教訓」的結果。

它拿掉了 C++ 中容易出錯的部分,加上了現代程式語言的優秀特性,同時保持了系統級程式語言的性能。

學習建議

  1. 如果你熟悉 C++:Rust 的概念不會太陌生,但需要適應所有權系統
  2. 如果你是新手:Rust 可能更適合作為第一門系統語言
  3. 選擇依據
    • 需要最大效能和控制:C++
    • 需要安全和現代特性:Rust
    • 維護大型代碼庫:Rust
    • 與現有 C++ 代碼集成:C++

Rust 模組系統完整指南

概述

Rust 沒有像 C/C++ 那樣的 .h 標頭檔,但有更強大的模組系統。本指南將詳細介紹 Rust 的模組系統,包括模組組織、路徑引用和最佳實踐。

C/C++ vs Rust 對比

C/C++Rust說明
.h 標頭檔.rs 模組檔都用來組織程式碼
#includemod + use引入其他檔案的內容
#ifndef 防重複自動防重複Rust 自動處理重複引入
前置宣告pub 關鍵字控制可見性
預處理器cfg 屬性條件編譯

專案結構範例

my_project/
├── Cargo.toml
└── src/
    ├── main.rs       # 程式入口點(crate root)
    ├── lib.rs        # 函式庫入口點
    ├── animals.rs    # 動物模組
    ├── swimming.rs   # 游泳相關模組
    └── animals/      # 子模組目錄
        ├── mod.rs    # 模組入口
        ├── fish.rs   # 魚類定義
        └── bird.rs   # 鳥類定義

路徑引用系統

crate 關鍵字

crate 是 Rust 中的特殊關鍵字,代表當前專案的根目錄,類似於檔案系統中的絕對路徑。

路徑引用方式

引用方式語法說明類比
絕對路徑crate::從專案根目錄開始「從大樓1樓開始」
相對路徑super::上一層模組「上一層樓」
當前模組self::同一模組內「同一層樓」
直接引用無前綴同層級模組「同一區域」

引用範例

#![allow(unused)]
fn main() {
// 絕對路徑(推薦)
use crate::swimming::CanSwim;
use crate::animals::Fish;
use crate::utils::helper::some_function;

// 相對路徑
use self::local_module::LocalStruct;    // 當前模組內
use super::parent_module::ParentStruct; // 父模組中

// 直接引用
use swimming::CanSwim;  // 同層級模組
}

完整程式碼範例

main.rs (crate root)

// 聲明模組
mod animals;          // 對應 animals.rs 檔案
mod swimming;         // 對應 swimming.rs 檔案

// 使用模組中的項目
use animals::{Fish, Bird};
use swimming::CanSwim;

fn main() {
    let fish = Fish::new("小丑魚".to_string(), "小丑魚".to_string());
    let bird = Bird::new("企鵝".to_string(), "國王企鵝".to_string());
    
    fish.swim();
    bird.swim();
}

swimming.rs

#![allow(unused)]
fn main() {
// 定義游泳相關的 trait

pub trait CanSwim {
    fn swim(&self);
}

pub trait CanDive {
    fn dive(&self, depth: f32);
}

// 提供預設實作
pub trait Aquatic {
    fn enter_water(&self) {
        println!("進入水中...");
    }
    
    fn exit_water(&self) {
        println!("離開水中...");
    }
}
}

animals.rs

#![allow(unused)]
fn main() {
// 動物結構體定義

// 使用絕對路徑引用(推薦)
use crate::swimming::CanSwim;

pub struct Fish {
    pub name: String,
    pub species: String,
    pub fin_count: u8,
    pub max_depth: f32,
}

pub struct Bird {
    pub name: String,
    pub species: String,
    pub wing_span: f32,
    pub can_fly: bool,
}

impl Fish {
    pub fn new(name: String, species: String) -> Self {
        Fish {
            name,
            species,
            fin_count: 6,
            max_depth: 30.0,
        }
    }
}

impl Bird {
    pub fn new(name: String, species: String) -> Self {
        Bird {
            name,
            species,
            wing_span: 85.0,
            can_fly: false,
        }
    }
}

// 實作 trait
impl CanSwim for Fish {
    fn swim(&self) {
        println!("{}({})用 {} 片鰭游泳!最深可潛到 {} 公尺", 
                self.name, self.species, self.fin_count, self.max_depth);
    }
}

impl CanSwim for Bird {
    fn swim(&self) {
        if self.can_fly {
            println!("{}({})翼展 {} 公分,在水面划水游泳!", 
                    self.name, self.species, self.wing_span);
        } else {
            println!("{}({})是不會飛的鳥類,用腳划水游泳!", 
                    self.name, self.species);
        }
    }
}
}

子模組結構

animals/mod.rs

#![allow(unused)]
fn main() {
// 子模組的入口點
pub mod fish;
pub mod bird;

// 重新導出供外部使用
pub use fish::Fish;
pub use bird::Bird;
}

animals/fish.rs

#![allow(unused)]
fn main() {
use crate::swimming::CanSwim;

pub struct Fish {
    pub name: String,
    pub species: String,
}

impl Fish {
    pub fn new(name: String, species: String) -> Self {
        Fish { name, species }
    }
}

impl CanSwim for Fish {
    fn swim(&self) {
        println!("{}({})游泳中...", self.name, self.species);
    }
}
}

外部依賴管理

Cargo.toml

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
clap = { version = "4.0", features = ["derive"] }

使用外部 crate

#![allow(unused)]
fn main() {
// 外部依賴
use serde::{Serialize, Deserialize};
use std::collections::HashMap;

// 內部模組
use crate::animals::Fish;
use crate::swimming::CanSwim;

#[derive(Serialize, Deserialize, Debug)]
pub struct Animal {
    name: String,
    species: String,
}
}

條件編譯

功能開關

#![allow(unused)]
fn main() {
// 在 Cargo.toml 中定義 features
// [features]
// advanced = []

#[cfg(feature = "advanced")]
mod advanced_swimming {
    pub trait AdvancedSwimming {
        fn underwater_breathing(&self);
        fn deep_dive(&self, depth: f32);
    }
}

#[cfg(feature = "advanced")]
pub use advanced_swimming::AdvancedSwimming;
}

除錯/發佈模式

#![allow(unused)]
fn main() {
#[cfg(debug_assertions)]
fn debug_info() {
    println!("這是 debug 模式 - 啟用額外檢查");
}

#[cfg(not(debug_assertions))]
fn debug_info() {
    // release 模式下什麼都不做
}

// 平台特定編譯
#[cfg(target_os = "windows")]
fn platform_specific() {
    println!("Windows 平台");
}

#[cfg(target_os = "linux")]
fn platform_specific() {
    println!("Linux 平台");
}
}

複雜模組結構範例

#![allow(unused)]
fn main() {
// 深層模組中的引用
// src/ocean/creatures/fish.rs

use crate::swimming::CanSwim;              // 絕對路徑到根模組
use crate::ocean::environment::Water;      // 絕對路徑到其他模組
use super::super::habitat::DeepSea;        // 相對路徑到祖父模組
use super::mammal::Whale;                  // 相對路徑到兄弟模組

pub struct DeepSeaFish {
    name: String,
    depth: f32,
}

impl DeepSeaFish {
    pub fn new(name: String, depth: f32) -> Self {
        DeepSeaFish { name, depth }
    }
}
}

最佳實踐

1. 使用絕對路徑

#![allow(unused)]
fn main() {
// ✅ 推薦:清晰明確
use crate::swimming::CanSwim;
use crate::animals::Fish;

// ❌ 不推薦:容易混淆
use super::super::swimming::CanSwim;
}

2. 合理的可見性控制

#![allow(unused)]
fn main() {
// 公開給外部使用
pub struct Fish {
    pub name: String,        // 公開欄位
    species: String,         // 私有欄位
}

impl Fish {
    pub fn new(name: String, species: String) -> Self {  // 公開方法
        Fish { name, species }
    }
    
    fn internal_method(&self) {  // 私有方法
        // 內部邏輯
    }
}
}

3. 重新導出(Re-export)

#![allow(unused)]
fn main() {
// 在 lib.rs 中重新導出常用項目
pub use animals::{Fish, Bird};
pub use swimming::CanSwim;

// 這樣外部使用者可以直接:
// use my_crate::{Fish, CanSwim};
}

4. 模組組織原則

  • 每個模組負責單一功能
  • 相關功能放在同一模組
  • 使用子模組組織複雜結構
  • 合理使用 pub 控制可見性

為什麼選擇 crate::

優勢

  1. 清晰明確 - 一看就知道是從專案根開始
  2. 避免混淆 - 區分內部模組和外部依賴
  3. 重構友好 - 移動檔案時不需要修改路徑
  4. 一致性 - 在任何模組中都使用相同的引用方式

範例對比

#![allow(unused)]
fn main() {
// 外部 crate vs 內部 crate
use serde::Serialize;           // 外部依賴
use std::collections::HashMap;  // 標準庫
use crate::animals::Fish;       // 你的專案內部模組
}

總結

Rust 的模組系統相比 C/C++ 的標頭檔有以下優勢:

  • 自動防重複引入 - 不需要 #ifndef 護衛
  • 更清晰的可見性 - pub 明確標示公開項目
  • 依賴管理 - Cargo 自動處理依賴關係
  • 編譯時檢查 - 模組錯誤在編譯時就會發現
  • 更好的組織 - 模組系統更直觀和安全

推薦做法: 在跨模組引用時優先使用 crate::,這樣程式碼更清晰、更好維護!

Rust 擁有權 vs 借用:生命週期問題指南 🦀

🔍 核心答案:只有借用才有生命週期問題!

簡單來說

  • 擁有權:我的東西,沒有生命週期問題
  • ⚠️ 借用:別人的東西,需要生命週期保護

📊 對比表格

類型是否需要生命週期範例說明
擁有類型❌ 不需要String, Vec<T>, u32我的東西,隨便用
借用類型✅ 需要&str, &Vec<T>, &u32別人的東西,要小心

💡 為什麼只有借用需要生命週期?

🏠 擁有權的本質

  • 擁有 = 我控制這塊記憶體
  • 我可以決定什麼時候釋放
  • 不會有懸空指標問題

📞 借用的本質

  • 借用 = 指向別人記憶體的指標
  • 如果原主人消失了,指標就變成「懸空指標」💥
  • 生命週期確保原主人活得夠久

✅ 擁有權:沒有生命週期問題

#![allow(unused)]
fn main() {
struct Person {
    name: String,    // 擁有 String,沒有生命週期標註
    age: u32,       // 擁有 u32,沒有生命週期標註
}

fn create_person() -> Person {
    Person {
        name: "小明".to_string(),  // 創建新的 String
        age: 25,
    }
    // 返回擁有權,完全沒問題!✅
}

// 更多擁有權例子
struct Config {
    host: String,        // 擁有
    port: u16,          // 擁有
    users: Vec<String>, // 擁有
    settings: HashMap<String, String>, // 擁有
}

impl Config {
    fn new(host: String, port: u16) -> Self {
        Config {
            host,
            port,
            users: Vec::new(),
            settings: HashMap::new(),
        }
    }
    
    // 所有方法都不需要生命週期標註
    fn get_host(&self) -> &str {
        &self.host
    }
}
}

⚠️ 借用:需要生命週期標註

#![allow(unused)]
fn main() {
struct PersonRef<'a> {
    name: &'a str,   // 借用,需要 'a
    age: u32,        // 擁有,不需要 'a
}

// 這樣會編譯失敗!❌
fn create_person_ref() -> PersonRef {
    let name = "小明".to_string();
    PersonRef {
        name: &name,  // 錯誤!name 會被銷毀
        age: 25,
    }
}

// 正確的借用用法 ✅
fn use_borrowed_data() {
    let name = "小明";  // 字串字面量,生命週期很長
    let person = PersonRef {
        name: &name,    // 可以借用
        age: 25,
    };
    println!("{} is {} years old", person.name, person.age);
    // name 活得比 person 久,所以安全
}
}

🎯 實際範例對比

方案 A:全用擁有權(推薦給初學者)

#![allow(unused)]
fn main() {
struct DatabaseConfig {
    host: String,           // 擁有
    username: String,       // 擁有
    password: String,       // 擁有
    database_name: String,  // 擁有
}

struct Application {
    name: String,           // 擁有
    version: String,        // 擁有
    config: DatabaseConfig, // 擁有
}

impl Application {
    fn new(name: String, version: String, config: DatabaseConfig) -> Self {
        Application { name, version, config }
    }
    
    // 完全沒有生命週期問題!
    fn get_connection_string(&self) -> String {
        format!("{}@{}/{}", 
                self.config.username, 
                self.config.host, 
                self.config.database_name)
    }
}
}

方案 B:混合借用(需要處理生命週期)

#![allow(unused)]
fn main() {
struct DatabaseConfigRef<'a> {
    host: &'a str,           // 借用 - 需要 'a
    username: &'a str,       // 借用 - 需要 'a
    password: &'a str,       // 借用 - 需要 'a
    database_name: &'a str,  // 借用 - 需要 'a
}

struct ApplicationRef<'a> {
    name: &'a str,                    // 借用 - 需要 'a
    version: &'a str,                 // 借用 - 需要 'a
    config: &'a DatabaseConfigRef<'a>, // 借用 - 需要 'a
}

impl<'a> ApplicationRef<'a> {
    fn new(name: &'a str, version: &'a str, config: &'a DatabaseConfigRef<'a>) -> Self {
        ApplicationRef { name, version, config }
    }
    
    // 返回的字串也需要生命週期標註
    fn get_connection_string(&self) -> String {
        format!("{}@{}/{}", 
                self.config.username, 
                self.config.host, 
                self.config.database_name)
    }
}
}

🚀 什麼時候該用借用?

✅ 適合借用的情況

  1. 短期使用:函數參數傳遞
  2. 避免複製:大型資料結構
  3. 效能優化:避免不必要的記憶體分配
#![allow(unused)]
fn main() {
// 函數參數借用 - 很常見且安全
fn print_info(name: &str, age: u32) {
    println!("{} is {} years old", name, age);
}

// 處理大型資料時借用
fn process_large_data(data: &Vec<u8>) -> usize {
    data.len()  // 只是讀取,不需要擁有
}
}

❌ 不適合借用的情況

  1. 長期存儲:結構體字段
  2. 返回值:從函數返回
  3. 複雜的所有權關係

🚨 特殊情況:函數參數和返回值

❌ 不寫生命週期會編譯失敗的情況

#![allow(unused)]
fn main() {
// 這樣寫會編譯錯誤!
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
}

編譯器錯誤訊息

error[E0106]: missing lifetime specifier
 --> src/lib.rs:1:37
  |
1 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, 
          but the signature does not say whether it is borrowed from `x` or `y`

🤔 編譯器的困惑

編譯器不知道:

  • 返回的 &str 是來自 x 還是 y
  • 如果來自 x,那 x 要活多久?
  • 如果來自 y,那 y 要活多久?
  • 我該如何檢查生命週期安全?😵

✅ 正確的寫法

#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
}

編譯器現在理解了

  • xy 都有相同的生命週期 'a
  • 返回值也是 'a 生命週期
  • 返回的引用不會比 xy 活得更久 ✅

🎯 實際使用範例

fn main() {
    let string1 = "long string is long";
    
    {
        let string2 = "xyz";
        let result = longest(string1, string2);
        println!("The longest string is {}", result);
        // result 在這裡還可以使用,因為 string1 和 string2 都還活著
    }
    // string2 死了,但沒關係,我們已經用完 result 了
}

💡 不想寫生命週期的替代方案

方案 1:返回擁有權

#![allow(unused)]
fn main() {
fn longest_owned(x: &str, y: &str) -> String {
    if x.len() > y.len() {
        x.to_string()  // 創建新的 String
    } else {
        y.to_string()  // 創建新的 String
    }
}
}

方案 2:返回索引或布林值

#![allow(unused)]
fn main() {
fn longest_index(x: &str, y: &str) -> bool {
    x.len() > y.len()  // 返回 true 表示 x 比較長
}
}

方案 3:使用靜態字串

#![allow(unused)]
fn main() {
fn longest_static() -> &'static str {
    "這是一個靜態字串"  // 'static 生命週期,活到程式結束
}
}

🧠 記憶口訣

生命週期必須寫的情況

  • 函數接收引用 + 返回引用 → 必須寫生命週期標註 ⚠️
  • 只接收引用,不返回引用 → 不需要寫 ✅
  • 返回擁有權 → 不需要寫 ✅

簡單判斷法

  • 如果是 & 開頭 → 可能需要生命週期 ⏰
  • 如果沒有 & → 不需要生命週期 ✅
  • 函數簽名有借用進也有借用出 → 一定需要生命週期 🚨

實用建議

  1. 初學者策略:多用擁有權,少用借用
  2. 進階優化:理解後再使用借用提升效能
  3. 記住原則:編譯器是你的朋友,會阻止記憶體錯誤
  4. 函數設計:如果可能,優先返回擁有權而不是借用

📝 總結

概念特徵生命週期使用場景
擁有權我的東西,完全控制❌ 不需要結構體字段、返回值、長期存儲
借用別人的東西,臨時使用✅ 需要函數參數、短期操作、效能優化

核心理解

  • 🏠 擁有權 = 房子是我的,我決定什麼時候拆
  • 📞 借用 = 借朋友的房子,朋友搬家前我就得搬出來

實用原則: 生命週期只是 Rust 確保「借用安全」的機制。如果你都用擁有權,就完全不用擔心生命週期問題!

Rust 所有權系統 - 生活化詳解

🏠 用房子來理解所有權

想像所有權就像是房子的房契:

基本規則(用房子比喻)

  1. 每間房子都有一個房主 - 每個值都有一個所有者
  2. 一間房子同時只能有一個房主 - 值在任一時刻只有一個所有者
  3. 房主搬走時,房子就被拆除 - 所有者離開作用域時,值被釋放
#![allow(unused)]
fn main() {
{
    // 這裡還沒有房子
    let my_house = String::from("溫馨小屋");  // 我買了一間房子
    // 我可以使用我的房子
}   // 我搬走了,房子被拆除
}

📦 Move:轉移房契

#![allow(unused)]
fn main() {
let 小明的房子 = String::from("兩房一廳");
let 小華的房子 = 小明的房子;  // 小明把房契轉給小華

// println!("{}", 小明的房子);  // ❌ 小明已經沒有房子了!
println!("{}", 小華的房子);     // ✅ 小華現在是房主
}

為什麼這樣設計?
就像現實中,房契轉移後,原房主就不能再使用房子了。這避免了兩個人同時認為自己擁有同一間房子的混亂。

🖨️ Clone:蓋一間一模一樣的房子

#![allow(unused)]
fn main() {
let 小明的房子 = String::from("豪華別墅");
let 小華的房子 = 小明的房子.clone();  // 小華蓋了一間一樣的房子

println!("小明住在:{}", 小明的房子);  // ✅ 小明還有自己的房子
println!("小華住在:{}", 小華的房子);  // ✅ 小華也有自己的房子
}

📋 Copy:影印身分證

對於簡單的資料(像數字),就像影印身分證一樣簡單:

#![allow(unused)]
fn main() {
let 小明的年齡 = 25;
let 小華的年齡 = 小明的年齡;  // 就像影印一份,原本還在

println!("小明 {} 歲", 小明的年齡);  // ✅ 原本還在
println!("小華 {} 歲", 小華的年齡);  // ✅ 影印本也能用
}

🎁 函數:把東西給朋友

送禮物(移轉所有權)

fn main() {
    let 我的禮物 = String::from("生日蛋糕");
    送給朋友(我的禮物);  // 我把禮物送出去了
    
    // println!("{}", 我的禮物);  // ❌ 我已經沒有禮物了
    
    let 我的錢 = 100;
    借給朋友(我的錢);  // 錢被「複製」了(因為數字很簡單)
    println!("我還有 {} 元", 我的錢);  // ✅ 我的錢還在
}

fn 送給朋友(禮物: String) {
    println!("朋友收到:{}", 禮物);
}  // 朋友用完禮物後,禮物就消失了

fn 借給朋友(錢: i32) {
    println!("朋友借到 {} 元", 錢);
}

朋友還禮物

fn 交換禮物(舊禮物: String) -> String {
    println!("收到:{}", 舊禮物);
    String::from("回禮")  // 朋友還我一個新禮物
}

fn main() {
    let 我的禮物 = String::from("巧克力");
    let 新禮物 = 交換禮物(我的禮物);  // 我給出巧克力,得到回禮
    println!("我現在有:{}", 新禮物);
}

🔍 引用:借用與歸還

不可變借用:借來看看

fn main() {
    let 我的書 = String::from("哈利波特");
    let 頁數 = 數頁數(&我的書);  // 朋友借去數頁數,但會還回來
    
    println!("《{}》有 {} 頁", 我的書, 頁數);  // ✅ 書還在我這裡
}

fn 數頁數(書: &String) -> usize {  // & 表示「借用」
    書.len()  // 只是看看,不會弄壞書
}  // 函數結束時,自動把書還回去

可變借用:借來修改

fn main() {
    let mut 我的筆記 = String::from("今天天氣很好");
    加註解(&mut 我的筆記);  // 朋友借去加註解
    
    println!("{}", 我的筆記);  // ✅ 筆記更新了
}

fn 加註解(筆記: &mut String) {  // &mut 表示「可以修改的借用」
    筆記.push_str(",適合出遊!");
}

🚨 借用規則:避免混亂

規則 1:不能同時有多個可變借用

#![allow(unused)]
fn main() {
let mut 共同帳戶 = String::from("餘額:1000元");

let 小明在用 = &mut 共同帳戶;  // 小明在使用帳戶
// let 小華也要用 = &mut 共同帳戶;  // ❌ 小華不能同時使用!

小明在用.push_str(" - 小明存款100元");
}

為什麼? 就像銀行帳戶,不能讓兩個人同時修改,會造成資料混亂!

規則 2:可變借用期間,原主人不能使用

#![allow(unused)]
fn main() {
let mut 我的日記 = String::from("今天很開心");

let 朋友在寫 = &mut 我的日記;
// println!("{}", 我的日記);  // ❌ 朋友在寫的時候,我不能看

朋友在寫.push_str(",因為學會了 Rust!");
// 朋友寫完後,我才能再次使用日記
}

⚠️ 懸垂引用:指向不存在的東西

#![allow(unused)]
fn main() {
fn 危險的函數() -> &String {  // ❌ 這樣寫會出錯
    let 臨時筆記 = String::from("這是臨時筆記");
    &臨時筆記  // 試圖借出臨時筆記的使用權
}  // 但臨時筆記在這裡就被銷毀了!

// 就像朋友說「你可以用我的車」
// 但說完就把車賣掉了 - 你拿到的是無效的車鑰匙!
}

正確做法:

#![allow(unused)]
fn main() {
fn 安全的函數() -> String {  // ✅ 直接把所有權轉移出去
    let 筆記 = String::from("這是真正的筆記");
    筆記  // 把整個筆記所有權轉移出去
}
}

🎯 實用技巧

1. 字串切片:借用部分內容

#![allow(unused)]
fn main() {
let 完整句子 = String::from("學習 Rust 很有趣");
let 部分內容 = &完整句子[0..2];  // 借用「學習」這兩個字
println!("{}", 部分內容);  // 輸出:學習
}

2. 函數參數建議

#![allow(unused)]
fn main() {
// ❌ 不好的做法:搶走所有權
fn 處理文字(文字: String) -> String {
    // 處理文字...
    文字
}

// ✅ 好的做法:只是借用
fn 處理文字(文字: &str) -> usize {
    文字.len()
}
}

🧠 記憶口訣

  1. 一個值,一個主人 - 避免混亂
  2. 借用要歸還 - 引用不能比原主人活得更久
  3. 可變借用獨佔 - 修改時不能有其他人使用
  4. 編譯器是好朋友 - 它會提醒你避免錯誤

🎉 為什麼這樣設計很棒?

  1. 記憶體安全:不會有懸垂指標或雙重釋放
  2. 執行緒安全:不會有資料競爭
  3. 零成本抽象:編譯時檢查,執行時沒有額外開銷
  4. 明確的資源管理:你總是知道誰擁有什麼

這就是為什麼 Rust 既安全又快速的秘密!

Rust 所有權完整指南 - 從基礎到精通 🦀

🎯 核心概念速覽

最重要的理解

  • 擁有權 (Ownership):我的東西,沒有生命週期問題
  • ⚠️ 借用 (Borrowing):別人的東西,需要生命週期保護
  • 📊 記憶體管理:Stack vs Heap 決定了不同的行為
  • 🔄 資料複製:Copy vs Clone 的關鍵差異

📚 第一部分:記憶體基礎 - Stack vs Heap

🏗️ Stack(堆疊)- 快速且簡單

特徵

  • 速度快,像疊盤子一樣 LIFO(後進先出)
  • 大小在編譯時就知道
  • 自動管理,不需要手動清理
  • 沒有 ownership 問題!

存放內容

#![allow(unused)]
fn main() {
// 這些都存在 Stack 上
let age: i32 = 25;           // 4 bytes,固定大小
let height: f64 = 175.5;     // 8 bytes,固定大小
let is_student: bool = true; // 1 byte,固定大小
let coordinates: (i32, i32) = (10, 20); // 8 bytes,固定大小

// 這些是 Stack 上的「指標」,指向 Heap 的資料
let name: String = String::from("小明");  // String 本身在 Stack,內容在 Heap
let numbers: Vec<i32> = vec![1, 2, 3];   // Vec 本身在 Stack,內容在 Heap
}

🏠 Heap(堆積)- 靈活但複雜

特徵

  • 較慢,需要記憶體分配器尋找空間
  • 大小可以在執行時改變
  • 需要手動管理(Rust 幫你做)
  • 這裡才有 ownership 問題!

存放內容

#![allow(unused)]
fn main() {
// String 的資料存在 Heap
let mut message = String::from("Hello");
message.push_str(" World");  // 可以動態增長

// Vec 的資料存在 Heap  
let mut numbers = Vec::new();
numbers.push(1);  // 可以動態添加元素
numbers.push(2);
}

🧠 記憶體布局視覺化

Stack                    Heap
┌─────────────┐         ┌──────────────────┐
│ age: 25     │         │                  │
├─────────────┤         │  "Hello World"   │ ← message 指向這裡
│ message: ●──┼────────→│  (11 bytes)      │
├─────────────┤         │                  │
│ numbers: ●──┼────────→│  [1, 2, 3]       │
└─────────────┘         │  (12 bytes)      │
                        └──────────────────┘

🎭 第二部分:Copy vs Clone - 資料複製的兩種方式

📋 Copy Trait - 像影印身分證

什麼是 Copy

  • 在 Stack 上的簡單位元複製
  • 非常快速,像影印一樣
  • 自動發生,不需要手動呼叫
  • 原始變數仍然有效

哪些類型實作了 Copy

#![allow(unused)]
fn main() {
// 基本數值類型 - 都實作了 Copy
let x: i32 = 5;
let y = x;  // 自動 copy,x 仍然可用
println!("x = {}, y = {}", x, y);  // ✅ 都可以用

// 其他 Copy 類型
let a: u32 = 10;
let b: f64 = 3.14;
let c: bool = true;
let d: char = '🦀';
let e: (i32, i32) = (1, 2);  // 如果元素都是 Copy,tuple 也是 Copy

// 陣列(如果元素是 Copy)
let arr1: [i32; 3] = [1, 2, 3];
let arr2 = arr1;  // Copy
println!("{:?} {:?}", arr1, arr2);  // ✅ 都可以用
}

為什麼這些類型可以 Copy

  • 它們的大小固定且已知
  • 都存在 Stack 上
  • 複製成本很低
  • 沒有指向 Heap 的指標

🖨️ Clone Trait - 蓋一間一模一樣的房子

什麼是 Clone

  • 深度複製,包括 Heap 上的資料
  • 可能很耗時和記憶體
  • 需要手動呼叫 .clone()
  • 創建完全獨立的副本
#![allow(unused)]
fn main() {
// String 需要 Clone(因為資料在 Heap)
let s1 = String::from("Hello");
let s2 = s1.clone();  // 手動 clone
println!("s1 = {}, s2 = {}", s1, s2);  // ✅ 都可以用

// Vec 需要 Clone
let v1 = vec![1, 2, 3];
let v2 = v1.clone();  // 複製整個向量和所有元素
println!("v1 = {:?}, v2 = {:?}", v1, v2);  // ✅ 都可以用

// 複雜結構的 Clone
#[derive(Clone)]
struct Person {
    name: String,
    age: i32,
}

let person1 = Person {
    name: String::from("小明"),
    age: 25,
};
let person2 = person1.clone();  // 深度複製所有字段
}

⚖️ Copy vs Clone 對比表

特徵CopyClone
觸發方式自動(賦值時)手動(.clone()
速度非常快可能較慢
記憶體只複製 Stack可能複製 Heap
原變數仍然有效仍然有效
適用類型簡單類型所有實作 Clone 的類型
成本幾乎為零取決於資料大小

🔍 實際例子:何時用 Copy vs Clone

#![allow(unused)]
fn main() {
fn copy_example() {
    let x = 42;
    let y = x;  // Copy 自動發生
    
    println!("x: {}, y: {}", x, y);  // 兩個都能用
    // 沒有性能問題,因為只是複製了 4 bytes
}

fn clone_example() {
    let big_string = "A".repeat(1_000_000);  // 100萬個字元
    let another_string = big_string.clone();  // 手動 clone
    
    // 這會複製 1MB 的資料!考慮是否真的需要
    println!("Original length: {}", big_string.len());
    println!("Clone length: {}", another_string.len());
}

fn better_approach() {
    let big_string = "A".repeat(1_000_000);
    let string_ref = &big_string;  // 借用,不複製
    
    // 只是借用,沒有複製成本
    println!("Length: {}", string_ref.len());
}
}

🏠 第三部分:Ownership 系統詳解

📜 基本規則(用房子比喻)

  1. 每個值都有一個所有者 - 每間房子都有房主
  2. 同時只能有一個所有者 - 一間房子只能有一個房主
  3. 所有者離開時,值被銷毀 - 房主搬走,房子拆除

🚨 重要:Ownership 只發生在 Heap 資料!

#![allow(unused)]
fn main() {
// Stack 資料 - 沒有 ownership 問題
let x = 5;
let y = x;  // Copy,兩個都有效
println!("{} {}", x, y);  // ✅ 完全沒問題

// Heap 資料 - 有 ownership 問題
let s1 = String::from("hello");
let s2 = s1;  // Move!s1 失效
// println!("{}", s1);  // ❌ 編譯錯誤!s1 已經無效
println!("{}", s2);     // ✅ s2 有效
}

為什麼只有 Heap 資料有 ownership 問題?

  1. Stack 資料

    • 大小固定,複製成本低
    • 自動管理,作用域結束就清理
    • 可以安全地複製多份
  2. Heap 資料

    • 大小可變,複製成本高
    • 需要明確的清理策略
    • 多個指標指向同一塊記憶體會造成問題

📦 Move:轉移所有權

#![allow(unused)]
fn main() {
fn move_example() {
    let house = String::from("豪華別墅");  // house 是房主
    let new_owner = house;               // 房契轉移給 new_owner
    
    // println!("{}", house);  // ❌ house 已經不是房主了
    println!("{}", new_owner); // ✅ new_owner 現在是房主
    
    // 函數調用也會 move
    take_ownership(new_owner);  // new_owner 的所有權轉移到函數內
    // println!("{}", new_owner);  // ❌ new_owner 已經無效
}

fn take_ownership(some_string: String) {
    println!("{}", some_string);
}  // some_string 在這裡被銷毀
}

🔗 第四部分:借用 (Borrowing) 與生命週期

🎯 核心概念:只有借用才有生命週期問題!

類型生命週期範例說明
擁有類型❌ 不需要String, Vec<T>, i32我的東西,隨便用
借用類型✅ 需要&str, &Vec<T>, &i32別人的東西,要小心

🔍 不可變借用:借來看看

#![allow(unused)]
fn main() {
fn immutable_borrow() {
    let book = String::from("Rust 程式設計");
    let page_count = count_pages(&book);  // 借用去數頁數
    
    println!("《{}》有 {} 頁", book, page_count);  // ✅ book 還在
}

fn count_pages(book_ref: &String) -> usize {
    book_ref.len()  // 只是看看,不修改
}
}

✏️ 可變借用:借來修改

#![allow(unused)]
fn main() {
fn mutable_borrow() {
    let mut note = String::from("今天學 Rust");
    add_comment(&mut note);  // 可變借用
    
    println!("{}", note);    // ✅ note 被修改了
}

fn add_comment(note_ref: &mut String) {
    note_ref.push_str(",很有趣!");
}
}

🚨 借用規則

  1. 可以有多個不可變借用
  2. 只能有一個可變借用
  3. 可變借用期間,不能有其他借用
#![allow(unused)]
fn main() {
fn borrowing_rules() {
    let mut data = String::from("資料");
    
    // ✅ 多個不可變借用 OK
    let r1 = &data;
    let r2 = &data;
    println!("{} 和 {}", r1, r2);
    
    // ✅ 可變借用(在不可變借用結束後)
    let r3 = &mut data;
    r3.push_str("更新");
    println!("{}", r3);
    
    // ❌ 這樣會錯誤:同時有可變和不可變借用
    // let r4 = &data;
    // let r5 = &mut data;  // 錯誤!
}
}

⚠️ 第五部分:生命週期詳解

🤔 什麼時候需要生命週期標註?

函數接收引用並返回引用 → 必須寫生命週期標註!

#![allow(unused)]
fn main() {
// ❌ 編譯錯誤:缺少生命週期標註
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

// ✅ 正確:明確生命週期關係
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
}

🧠 編譯器的困惑

編譯器需要知道:

  • 返回的引用來自哪個參數?
  • 這個引用能活多久?
  • 如何確保不會產生懸空指標?
#![allow(unused)]
fn main() {
fn lifetime_example() {
    let string1 = "long string is long";
    
    {
        let string2 = "xyz";
        let result = longest(string1, string2);
        println!("最長的是:{}", result);
        // result 在這裡還能用,因為兩個輸入都還活著
    }
    // string2 死了,但沒關係,我們已經用完 result
}
}

💡 避免生命週期的方法

方案 1:返回擁有權

#![allow(unused)]
fn main() {
fn longest_owned(x: &str, y: &str) -> String {
    if x.len() > y.len() {
        x.to_string()  // 創建新的 String
    } else {
        y.to_string()
    }
}
}

方案 2:返回其他資訊

#![allow(unused)]
fn main() {
fn is_first_longer(x: &str, y: &str) -> bool {
    x.len() > y.len()
}
}

📋 第六部分:結構體中的選擇

✅ 方案 A:全用擁有權(推薦新手)

#![allow(unused)]
fn main() {
struct Person {
    name: String,        // 擁有
    email: String,       // 擁有
    age: u32,           // 擁有(Copy 類型)
}

impl Person {
    fn new(name: String, email: String, age: u32) -> Self {
        Person { name, email, age }
    }
    
    // 完全沒有生命週期問題!
    fn introduction(&self) -> String {
        format!("我是 {},{}歲,email: {}", 
                self.name, self.age, self.email)
    }
}

fn owned_example() {
    let person = Person::new(
        "小明".to_string(),
        "ming@example.com".to_string(),
        25
    );
    
    println!("{}", person.introduction());
}
}

⚠️ 方案 B:使用借用(需要生命週期)

#![allow(unused)]
fn main() {
struct PersonRef<'a> {
    name: &'a str,       // 借用 - 需要 'a
    email: &'a str,      // 借用 - 需要 'a  
    age: u32,           // 擁有 - 不需要 'a
}

impl<'a> PersonRef<'a> {
    fn new(name: &'a str, email: &'a str, age: u32) -> Self {
        PersonRef { name, email, age }
    }
    
    fn introduction(&self) -> String {
        format!("我是 {},{}歲,email: {}", 
                self.name, self.age, self.email)
    }
}

fn borrowed_example() {
    let name = "小華";
    let email = "hua@example.com";
    
    let person = PersonRef::new(name, email, 30);
    println!("{}", person.introduction());
    // name 和 email 必須比 person 活得更久
}
}

🎯 第七部分:實用指南

🚀 什麼時候用借用?

✅ 適合借用的情況

  1. 函數參數 - 避免不必要的所有權轉移
  2. 大型資料 - 避免昂貴的複製
  3. 短期使用 - 臨時操作
#![allow(unused)]
fn main() {
// 函數參數借用
fn print_info(name: &str, age: u32) {
    println!("{} 今年 {} 歲", name, age);
}

// 處理大型資料
fn analyze_data(data: &Vec<u8>) -> usize {
    data.len()  // 只需要讀取,不需要擁有
}

// 字串切片
fn get_first_word(text: &str) -> &str {
    match text.find(' ') {
        Some(index) => &text[..index],
        None => text,
    }
}
}

❌ 不適合借用的情況

  1. 結構體字段 - 複雜的生命週期管理
  2. 返回值 - 避免懸空引用
  3. 長期存儲 - 所有權更清晰

🧠 記憶口訣與決策樹

生命週期決策樹

是否需要寫生命週期標註?
├─ 有 `&` 符號嗎?
│  ├─ 沒有 → ❌ 不需要
│  └─ 有 → 繼續判斷
│      ├─ 函數接收引用並返回引用?
│      │  ├─ 是 → ✅ 需要生命週期標註
│      │  └─ 否 → ❌ 不需要
│      └─ 結構體存儲引用?
│         └─ 是 → ✅ 需要生命週期標註

記憶口訣

  • Stack 資料 Copy,Heap 資料 Move
  • 擁有權在 Heap,Copy 在 Stack
  • 借用要歸還,生命週期保安全
  • 函數進出都是引用,生命週期必須標

🎨 實用範例:配置管理

// 推薦:全擁有權版本
#[derive(Debug, Clone)]
struct Config {
    database_url: String,
    api_key: String,
    max_connections: u32,
    debug_mode: bool,
}

impl Config {
    fn from_env() -> Self {
        Config {
            database_url: std::env::var("DATABASE_URL")
                .unwrap_or_else(|_| "localhost:5432".to_string()),
            api_key: std::env::var("API_KEY")
                .unwrap_or_else(|_| "default_key".to_string()),
            max_connections: 10,
            debug_mode: false,
        }
    }
    
    fn connection_string(&self) -> String {
        format!("{}?max_conn={}", self.database_url, self.max_connections)
    }
}

// 使用
fn main() {
    let config = Config::from_env();
    println!("設定:{:?}", config);
    println!("連接字串:{}", config.connection_string());
    
    // 可以輕鬆複製配置
    let backup_config = config.clone();
    println!("備份設定:{:?}", backup_config);
}

📊 第八部分:性能考慮

⚡ 性能對比

操作StackHeap說明
分配極快較慢Stack 只需移動指標
存取極快較慢Stack 有更好的局部性
複製Stack 是簡單的記憶體複製
清理自動自動Rust 的 RAII 系統

🔧 優化建議

#![allow(unused)]
fn main() {
// ❌ 不必要的分配
fn bad_example() -> String {
    let mut result = String::new();
    for i in 0..1000 {
        result = format!("{}{}", result, i);  // 每次都重新分配!
    }
    result
}

// ✅ 更好的方法
fn good_example() -> String {
    let mut result = String::with_capacity(4000);  // 預分配容量
    for i in 0..1000 {
        result.push_str(&i.to_string());
    }
    result
}

// ✅ 最佳方法(如果可能的話)
fn best_example(buffer: &mut String) {
    buffer.clear();
    for i in 0..1000 {
        buffer.push_str(&i.to_string());
    }
}
}

🏆 總結:掌握 Rust 所有權的關鍵

🎯 核心理解框架

  1. 記憶體模型

    • Stack = 快速 + 自動管理 + Copy
    • Heap = 靈活 + 手動管理 + Move
  2. 所有權規則

    • 只有 Heap 資料有所有權問題
    • Stack 資料自動 Copy,沒有所有權轉移
  3. 借用系統

    • 不可變借用:隨意多個
    • 可變借用:獨佔一個
    • 生命週期確保安全
  4. 生命週期標註

    • 只在必要時使用
    • 函數輸入輸出都是引用時需要
    • 結構體存儲引用時需要

🚀 實踐建議

初學者策略

  1. 優先使用擁有權類型(String, Vec, etc.)
  2. 函數參數使用借用(&str, &[T], etc.)
  3. 避免在結構體中存儲引用
  4. 理解 Copy vs Clone 的差異

進階優化

  1. 合理使用借用減少複製
  2. 理解生命週期標註的時機
  3. 選擇合適的資料結構
  4. 考慮性能影響

記憶要點

  • 🏠 擁有權 = 房子是我的,我控制何時拆除
  • 📞 借用 = 朋友借房子,朋友搬家前要還回來
  • 📋 Copy = 像影印身分證,簡單快速
  • 🖨️ Clone = 蓋一間一樣的房子,費時費力
  • 🧠 生命週期 = 確保借用安全的編譯時檢查

Rust 的所有權系統看似複雜,但掌握這些核心概念後,你就能寫出既安全又高效的程式碼!🎉

Rust 記憶體配置指南:Stack vs Heap

在 Rust 中,變數的記憶體配置有其獨特的規則,主要由所有權系統型別特性決定。

Stack 上的變數

實作 Copy trait 的型別

fn main() {
    let x = 42;           // i32, stack 上
    let y = 3.14;         // f64, stack 上
    let flag = true;      // bool, stack 上
    let ch = 'A';         // char, stack 上
    let tuple = (1, 2);   // (i32, i32), stack 上
}

固定大小的陣列

fn main() {
    let arr = [1, 2, 3, 4, 5];  // [i32; 5], stack 上
    let bytes = [0u8; 1024];    // [u8; 1024], stack 上
}

Heap 上的變數

使用 Box

fn main() {
    let boxed = Box::new(42);        // Box<i32>, 值在 heap
    let large_array = Box::new([0; 1000000]);  // 大陣列在 heap
}

Vec、String 等集合型別

fn main() {
    let mut vec = Vec::new();        // 資料在 heap
    vec.push(1);
    
    let s = String::from("hello");   // 字串資料在 heap
}

自定義結構體(預設在 stack)

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 };      // stack 上
    let p2 = Box::new(Point { x: 3, y: 4 }); // heap 上
}

判斷方法

1. 檢查型別特性

use std::mem;

fn main() {
    let x = 42;
    let s = String::from("hello");
    
    // 如果型別實作了 Copy,通常在 stack
    fn is_copy<T: Copy>() {}
    is_copy::<i32>();  // 編譯通過,i32 在 stack
    // is_copy::<String>();  // 編譯錯誤,String 不是 Copy
}

2. 觀察所有權轉移

fn main() {
    let x = 5;
    let y = x;  // Copy,x 仍可使用,說明在 stack
    println!("{}", x); // OK
    
    let s1 = String::from("hello");
    let s2 = s1;  // Move,s1 不可再使用,說明涉及 heap
    // println!("{}", s1); // 編譯錯誤
}

3. 使用記憶體分析工具

fn main() {
    let x = 42;
    let boxed = Box::new(42);
    
    println!("stack variable address: {:p}", &x);
    println!("box pointer address: {:p}", &boxed);
    println!("heap value address: {:p}", boxed.as_ref());
}

特殊情況

智慧指標

use std::rc::Rc;
use std::sync::Arc;

fn main() {
    let rc = Rc::new(42);     // 值在 heap,Rc 本身在 stack
    let arc = Arc::new(42);   // 值在 heap,Arc 本身在 stack
}

閉包捕獲

fn main() {
    let x = 42;  // stack
    
    let closure = move || {
        println!("{}", x);  // x 被移動到閉包中
    };
    // 閉包可能在 stack 或 heap,取決於使用方式
}

編譯器最佳化

Rust 編譯器會進行各種最佳化:

  • 逃逸分析:如果堆疊變數不會逃出函數範圍,可能保持在 stack
  • 內聯展開:小函數可能被內聯,影響變數位置
  • LLVM 最佳化:後端最佳化可能重新安排記憶體配置

實用檢查方法

要準確了解變數位置,可以:

  1. 查看型別是否實作 Copy trait
  2. 觀察所有權轉移行為
  3. 使用 cargo expand 查看巨集展開後的程式碼
  4. 使用記憶體分析工具如 Valgrind 或 heaptrack

快速判斷表

型別類型位置範例
基本型別 (i32, f64, bool, char)Stacklet x = 42;
固定陣列Stacklet arr = [1, 2, 3];
BoxHeap (值)let boxed = Box::new(42);
VecHeap (資料)let vec = vec![1, 2, 3];
StringHeap (資料)let s = String::from("hello");
&strStack (指標)let s = "hello";
自定義 structStack (預設)let p = Point { x: 1, y: 2 };

記憶要點

  • Copy trait: 實作此 trait 的型別通常在 stack
  • 所有權轉移: 發生 move 的型別通常涉及 heap
  • 明確配置: 使用 Box::new() 強制放在 heap
  • 編譯器智慧: 最終位置可能因最佳化而改變

Rust 包裝類型白話指南 📦

用最簡單的話解釋 Rust 的各種"盒子"

基礎概念 🎯

想像你有很多不同的盒子,每個盒子都有不同的特殊能力:

1. Box - 普通紙盒 📦

白話解釋: 把東西放到堆上的盒子,你是唯一的主人。

使用時機: 東西太大放不進棧,或者需要遞歸結構。

#![allow(unused)]
fn main() {
// 簡單例子
let big_data = Box::new([0; 1000000]); // 把大陣列放到堆上
println!("數據在堆上:{}", big_data.len());

// 遞歸結構(鏈表)
enum List {
    Node(i32, Box<List>),  // 沒有 Box 就編譯不過
    Empty,
}
}

關鍵特點:

  • ✅ 獨占所有權(只有你能用)
  • ✅ 自動清理(離開作用域就釋放)
  • ❌ 不能共享

2. Rc - 共享盒子(單線程)🔗

白話解釋: 可以被多個人同時使用的盒子,有引用計數器。

使用時機: 單線程中多個地方需要使用同一個數據。

#![allow(unused)]
fn main() {
use std::rc::Rc;

let data = Rc::new("共享數據".to_string());
let reference1 = data.clone();  // 引用計數 +1
let reference2 = data.clone();  // 引用計數 +1

println!("引用計數:{}", Rc::strong_count(&data)); // 輸出:3
println!("數據:{}", reference1);
}

關鍵特點:

  • ✅ 多個所有者
  • ✅ 自動清理(引用計數為 0 時)
  • ❌ 不能修改內容
  • ❌ 不是線程安全

3. Arc - 共享盒子(多線程)🌐

白話解釋: 線程安全版本的 Rc,可以在不同線程間傳遞。

使用時機: 多線程程序中共享數據。

#![allow(unused)]
fn main() {
use std::sync::Arc;
use std::thread;

let data = Arc::new("多線程共享數據".to_string());

let mut handles = vec![];
for i in 0..3 {
    let data_clone = data.clone();
    let handle = thread::spawn(move || {
        println!("線程 {} 看到:{}", i, data_clone);
    });
    handles.push(handle);
}

for handle in handles {
    handle.join().unwrap();
}
}

關鍵特點:

  • ✅ 多個所有者
  • ✅ 線程安全
  • ❌ 不能修改內容

4. RefCell - 魔法盒子(運行時借用)🎭

白話解釋: 可以在「不可變」的情況下修改內容的魔法盒子。

使用時機: 需要在不可變環境中修改數據。

#![allow(unused)]
fn main() {
use std::cell::RefCell;

let data = RefCell::new(42);

// 讀取
println!("值:{}", data.borrow()); // 42

// 修改
*data.borrow_mut() = 100;
println!("修改後:{}", data.borrow()); // 100

// 在不可變函數中修改
fn modify_data(cell: &RefCell<i32>) {  // 注意:不可變引用
    *cell.borrow_mut() = 999;  // 但還是能修改!
}
modify_data(&data);
}

關鍵特點:

  • ✅ 內部可變性
  • ✅ 運行時借用檢查
  • ❌ 違反規則會 panic
  • ❌ 不是線程安全

5. Cell - 簡單替換盒子 🔄

白話解釋: 只能整個替換,不能借用內部的簡單盒子。

使用時機: 簡單類型的內部可變性。

#![allow(unused)]
fn main() {
use std::cell::Cell;

let data = Cell::new(42);

println!("原值:{}", data.get()); // 42
data.set(100);                    // 整個替換
println!("新值:{}", data.get()); // 100

// 交換
let old = data.replace(200);
println!("舊值:{},新值:{}", old, data.get());
}

關鍵特點:

  • ✅ 比 RefCell 簡單
  • ✅ 沒有運行時借用檢查開銷
  • ❌ 只能整體替換
  • ❌ 不能借用內部值

6. Mutex - 帶鎖的盒子 🔒

白話解釋: 多線程環境下帶鎖的盒子,同時只能一個人用。

使用時機: 多線程中需要修改共享數據。

#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..5 {
    let counter = counter.clone();
    let handle = thread::spawn(move || {
        let mut num = counter.lock().unwrap(); // 獲取鎖
        *num += 1;
    }); // 鎖自動釋放
    handles.push(handle);
}

for handle in handles {
    handle.join().unwrap();
}

println!("最終計數:{}", *counter.lock().unwrap());
}

關鍵特點:

  • ✅ 線程安全
  • ✅ 內部可變性
  • ❌ 可能阻塞
  • ❌ 可能死鎖

7. Option - 可能空的盒子 📦❓

白話解釋: 可能有東西,也可能沒東西的盒子。

使用時機: 表示可能不存在的值。

#![allow(unused)]
fn main() {
fn find_number(numbers: &[i32], target: i32) -> Option<usize> {
    for (index, &num) in numbers.iter().enumerate() {
        if num == target {
            return Some(index);  // 找到了
        }
    }
    None  // 沒找到
}

let numbers = vec![1, 2, 3, 4, 5];

match find_number(&numbers, 3) {
    Some(index) => println!("找到了,在位置:{}", index),
    None => println!("沒找到"),
}

// 或者用更簡潔的方式
if let Some(index) = find_number(&numbers, 3) {
    println!("找到了,在位置:{}", index);
}
}

關鍵特點:

  • ✅ 強制處理空值情況
  • ✅ 沒有空指針異常
  • ✅ 表達力強

8. Result<T, E> - 可能出錯的盒子 ⚠️

白話解釋: 操作可能成功也可能失敗的盒子。

使用時機: 可能失敗的操作。

#![allow(unused)]
fn main() {
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("不能除零!".to_string())
    } else {
        Ok(a / b)
    }
}

match divide(10.0, 2.0) {
    Ok(result) => println!("結果:{}", result),
    Err(error) => println!("錯誤:{}", error),
}

// 或者用 ? 運算符傳播錯誤
fn calculate() -> Result<f64, String> {
    let result = divide(10.0, 2.0)?;  // 如果錯誤就直接返回錯誤
    Ok(result * 2.0)
}
}

關鍵特點:

  • ✅ 強制處理錯誤
  • ✅ 類型安全
  • ✅ 可以鏈式操作

9. Weak - 弱引用盒子 🔗💔

白話解釋: 不增加引用計數的「弱」引用,避免循環依賴。

使用時機: 避免 Rc 循環引用導致內存洩漏。

#![allow(unused)]
fn main() {
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Parent {
    children: RefCell<Vec<Rc<Child>>>,
}

struct Child {
    parent: RefCell<Weak<Parent>>,  // 用 Weak 避免循環
}

let parent = Rc::new(Parent {
    children: RefCell::new(vec![]),
});

let child = Rc::new(Child {
    parent: RefCell::new(Rc::downgrade(&parent)),  // 創建弱引用
});

parent.children.borrow_mut().push(child.clone());

// 通過弱引用訪問父節點
if let Some(parent_ref) = child.parent.borrow().upgrade() {
    println!("子節點找到了父節點!");
}
}

關鍵特點:

  • ✅ 避免循環引用
  • ✅ 自動清理
  • ❌ 可能訪問失敗

10. Cow - 寫時複製盒子 🐄

白話解釋: 平時借用,需要修改時才複製的聰明盒子。

使用時機: 大部分時候只讀,偶爾需要修改的情況。

#![allow(unused)]
fn main() {
use std::borrow::Cow;

fn process_text(input: &str) -> Cow<str> {
    if input.contains("bug") {
        // 需要修改,創建新的
        Cow::Owned(input.replace("bug", "feature"))
    } else {
        // 不需要修改,直接借用
        Cow::Borrowed(input)
    }
}

let text1 = "Hello world";
let result1 = process_text(text1);
println!("'{}',沒有複製", result1);

let text2 = "This is a bug";  
let result2 = process_text(text2);
println!("'{}',發生了複製", result2);
}

關鍵特點:

  • ✅ 性能優化
  • ✅ 按需複製
  • ✅ 透明使用

常用組合套餐 🍱

單線程共享可變數據

#![allow(unused)]
fn main() {
Rc<RefCell<T>>
}

說明: 多個地方共享,還能修改

多線程共享可變數據

#![allow(unused)]
fn main() {
Arc<Mutex<T>>
}

說明: 多線程共享,帶鎖修改

可選的共享數據

#![allow(unused)]
fn main() {
Option<Rc<T>>
}

說明: 可能沒有,如果有就是共享的

可能失敗的操作

#![allow(unused)]
fn main() {
Result<Option<T>, Error>
}

說明: 操作可能失敗,成功了也可能沒有值

選擇指南 🎯

需求選擇理由
堆分配Box<T>簡單直接
單線程共享Rc<T>多個所有者
多線程共享Arc<T>線程安全
內部可變性RefCell<T>運行時檢查
簡單內部可變性Cell<T>整體替換
多線程可變Mutex<T>帶鎖保護
可選值Option<T>可能沒有
錯誤處理Result<T,E>可能失敗
避免循環引用Weak<T>弱引用
性能優化Cow<T>寫時複製

記憶小貼士 💡

  • Box: 📦 = 普通盒子,一個主人
  • Rc: 🔗 = Reference Counting,多個主人(單線程)
  • Arc: 🌐 = Atomic Rc,多個主人(多線程)
  • RefCell: 🎭 = 運行時魔法,可變的不可變
  • Cell: 🔄 = 簡單替換
  • Mutex: 🔒 = 互斥鎖
  • Option: ❓ = 可能有可能沒有
  • Result: ⚠️ = 成功或失敗
  • Weak: 💔 = 弱引用,不算數
  • Cow: 🐄 = 寫時複製,聰明牛

記住:每個「盒子」都是為了解決特定問題,選對盒子事半功倍! 🚀

GO

Go 標準套件安裝

http://golang.org/dl/

export GOROOT=$HOME/go
export GOPATH=$HOME/gopath
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

Go Modules 指令介紹

Usage:go mod <command> [arguments]The commands are:download    // 將依賴全部下載到本機中,位置為 $GOPATH/pkg/mod/cache
edit        // 編輯 go.mod 例如鎖定某個依賴的版本
graph       // 列出專案中哪一個部分使用了某個依賴
init        // 建立 go.mod
tidy        // 增加遺失的依賴,移除未使用的依賴
vendor      // 將既有的 go.mod 依賴全部存在 /vendor 底下
verify      // 驗證本地依賴依然符合 go.sum 
why         // 解釋某個依賴為何存在在 go.mod 中,誰使用了它

Golang Note

Modules and Packages

不論是 module 或 package,都可以在本地匯入,不一定要發到遠端 repository。

# 在 hello 資料夾中
$ go mod init example.com/user/hello        # 宣告 module_path,通常會和該 repository 的 url 位置一致
$ go install .                      # 編譯該 module 並將執行檔放到 GOBIN,因此在 GOBIN 資料夾中會出現 hello 的執行檔

$ go mod tidy             # 移除沒用到的套件
$ go clean -modcache            # 移除所有下載第三方套件的內容

💡 安裝到 GOBIN 資料夾的檔案名稱,會是在 go.mod /案中第一行定義 module path 中路徑的最後一個。因此若 module_path 是 example.com/user/hello 則在 GOBIN 中的檔名會是 hello;若 module_path 是 example/user 則在 GOBIN 資料夾中的檔名會是 user

若我們在 go module 中有使用其他的遠端(第三方)套件,當執行 go installgo buildgo run 時,go 會自動下載該 remote module,並記錄在 go.mod 檔案中。這些遠端套件會自動下載到 $GOPATH/pkg/mod 的資料夾中。當有不同的 module 之間需要使用相同版本的第三方套件時,會共用這些下載的內容,因此這些內容會是「唯讀」。若想要刪除這些第三方套件的內容,可以輸入 go clean -modcache

設定 GOPATH

# ~/.zshrc
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin

Packages

  • Go 的程式碼都是以 package 的方式在組織,一個 package 就是在同一資料夾中的許多 GO 檔案。

  • 同一個 package 的所有 Go 檔案會放在同一個資料夾內;而這個資料夾內的所有檔案也都會屬於同一個 package,有相同的 package 名稱

  • 如果套件是在 module 中,go import package 的路徑會是 module path 加上 subdirectory

  • 通常 package 的名稱會跟著 folder 的名稱,舉例來說,若檔案放在 math/rand 資料夾中,則該套件會稱作 rand

  • package scope

    • 在 Go 語言中,並沒有區分 publicprivateprotected,而是根據變數名稱的第一個字母大小寫來判斷能否被外部引用。
    • 在同一個 package 中變數、函式、常數和 type,都隸屬於同一個 package scope,因此雖然可能在不同支檔案內,但只要隸屬於同一個 package,都可以使用(visible)。
    • 如果需要讓 package 內的變數或函式等能夠在 package 外部被使用,則該變數的第一個字母要大寫才能讓外部引用(Exported names),否則的話會無法使用
  • 有兩種不同類型的 package:

    • executable package:是用來產生我們可以執行的檔案,一定會包含 package main 並且帶有名為 main 的函式,只有這個檔案可以被執行(run)和編譯(build),並且不能被其他檔案給匯入。
    • reusable package(library package):類似 "helpers" 或常稱作 library / dependency,目的是可以放入可重複使用的程式邏輯,它是不能被直接執行的,可以使用任何的名稱。

在 Go 裡面要區別這兩種 Package 的主要方式就是利用「package 名稱」,當使用 main 當做 package 名稱時,就會被當作 executable package,因此接著執行 go build <fileName> 時,會產生一支執行檔;但是當使用 main 以外的名稱是,都會被當作是 reusable package,因此當使用 go build 指令時,不會產生任何檔案

要匯入 reusable package 只需要:

// 匯入單一個 package
import "fmt"

// 匯入多個 packages(不用逗號)
import (
  "fmt"
  "strings"
)
// go-hello-world/main.go
package main

import (
    "fmt"

    // 把 foo 這個 package 的方法都放到這隻檔案中,如此不用使用 foo.HelloWorld(不建議)
    . "go-hello-world/foo"

    // 將模組轉換為別名,可以使用 bar.HelloWorld
    bar "go-hello-world/foo"

    // 沒有用到這個 package,但要 init 它
    _ "go-hello-world/foo"
)

func main() {
    fmt.Println(bar.HelloWorld())
    fmt.Println("Hello main")
}
// go-hello-world/foo/helloworld.go

package foo

import "fmt"

func init() {
    fmt.Println("This is init of helloworld")
}

// HelloWorld ...
func HelloWorld() string {
    return "Hello World"
}

package name: coding style and convention

在 Go 中,package 的名稱應該是短而清楚,以小寫(lower case)命名,同時不包含底線(under_scores)或小寫駝峰(mixedCaps),並且通常會是名詞(noun),例如 timelist、或 http

在幫 package 命名的時候,試想自己就是使用該 pkg 的開發者,用這種角度來替自己的 pkg 命名。

另外,由於使用者在匯入該 package,假設引入的 使用時,一定會需要使用該 package 的 name 作為前綴,因此在 package 中的變數名稱盡可能不要和 package name 重複:

  • http package 中如果要使用 Server,不需要使用 http.HTTPServer,而是可以直接使用 http.Server

  • 當在

    uuid

    的 package 要產生一組

    uuid.UUID
    

    時,不需要使用

    uuid.NewUUID()
    

    的方法,而是可以直接使用

    uuid.New()
    

    ,也就是說如果回傳的型別名稱(

    UUID
    

    )和該 pkg 的名稱相同時,可以直接將該方法命名成

    New
    

    ,而不用是

    NewOOO
    
    • time.Now() 會回傳 time.Time
  • 如果 pkg 會回傳的 struct 名稱不同於 package 本身的名稱時,則可以使用

    NewOOO
    
    • time.NewTicker()
    • uuid.NewRandom()

💡 雖然 pkg 中 variable name 的前綴會盡量不和 package name 重複,但很常見的情況是在該 pkg 中有其同名的 struct,例如 time pkg 中有名為 Time 的 struct,因此型別會是 time.Time

不好的用法:

  • 盡可能不要使用 util, common, misc 這類的名稱作為 package name,因為這對使用者來說是沒有意義的名字,而是去想使用者這如果要用這些方法的話,最有可能使用到的關鍵字是什麼。
  • 見範例「Break up generic packages

go tool 找 package 的邏輯

go tool 會使用 $GOPATH 來找對應的 package,假設引入的 package 路徑是 "github.com/user/hello",那麼 go tool 就會去找 $GOPATH/src/github.com/user/hello

Modules

# 初始化 Go Module
$ export GO111MODULE=on  # 在 GOPATH 外要使用 module 需要啟動

# go mod init [module_path]
$ go mod init example.com/user/hello        # 宣告 module path
$ go mod tidy        # 移除沒用到的 library
$ go mod download    # 下載套件(go build 和 go test 也會自動下載)

$ go get [library]   # 新增或更新 package 到 Module 內
$ go get -u ./...    # 等同於,go get -u=patch ./...
$ go get foo@master. # 下載特定版本的 go package

$ go list -m all      # 印出 module 正在使用的所有套件
$ go list -m -versions [package]   # 列出所有此套件可下載的版本
$ go list -u -m all   # 檢視有無任何 minor 或 patch 的更新
  • 在一個專案中通常只會有一個 module(但也可以有多個),並且放在專案的根目錄,module 裡面會集合所有相關聯的 Go packages。在 go.mod 中會宣告 module path,這是用來匯入所有在此 module 中的路徑的前綴(path prefix),同時它也讓 go 的工具知道要去哪裡下載它。
  • 透過 Modules 可以準確紀錄相依的套件,讓程式能再次被編譯。
  • 總結來說:
    • 一個 repository 會包含一個或以上的 Go modules
    • 每個 module 會包含一個或以上的 Go packages
    • 每個 package 會包含一個或以上的檔案在單一資料夾中
  • 在執行 go buildgo test 時,會根據 imports 的內容自動添加套件,並更新 go.mod
  • 當需要的時候,可以直接在 go.mod 指定特定的版本,或使用 go get,例如 go get foo@v1.2.3, go get foo@master, go get foo@e3702bed2

go.mod

在 root directory 中會透過 go.mod 來定義 Module,而 Module 的原始碼可以放在 GOPATH 外,有四種指令 module, require, replace, exclude 可以使用:

// go.mod
// go.mod
module github.com/my/thing

require (
    github.com/some/dependency v1.2.3
    github.com/another/dependency/v4 v4.0.0
)

module

用來宣告 Module 的身份,並帶入 module 的路徑。在這個 module 中所有匯入的路徑都會以這個 module path 當作前綴(prefix)。透過 module 的路徑,以及 go.mod 到 package's 資料夾的相對路徑,會共同決定 import package's 時要使用的路徑。

replace and execute

這兩個命令都只能用在當前模組(即, main),否則將會在編譯時被忽略。

其他

  • 階層關係上:Module > Package > Directory

跨檔案引用函式

從下面的例子中可以看到,雖然 main.go 裡面有一個函式是定義在 state.go 的檔案中,但因為它們屬於同一個 package,所以當從 Terminal 執行 go run main.go state.go 時,程式可以正確執行。

或者也可以輸入 go run *.go

// main.go
package main

func main() {
  printState()
}
// state.go
package main

import "fmt"

func printState() {
  fmt.Println("California")
}

套件載入的流程

在 golang 中,使用某一個套件時,go 會先去 GOROOT 找看看是不是內建的函式庫,如果找不到的話,會去 GOPATH 內找,如果都找不到的話,就無法使用。

變數宣告(variables)

Go 屬於強型別(Static Types)的語言,其中常見的基本型別包含 bool, string, int, float64, map

第一種宣告方式(最常用):short declaration

使用 := 宣告,表示之前沒有進行宣告過。這是在 go 中最常使用的變數宣告的方式,因為它很簡潔。但因為在 package scope 的變數都是以 keyword 作為開頭,因此不能使用縮寫的方式定義變數(foo := bar),只能在 function 中使用,具有區域性(local variable):

// 第一種宣告方式
function main() {
    foo := "Hello"
    bar := 100

  // 也可以簡寫成
  foo, bar := "Hello", 100
}

// 等同於
function main() {
    var foo string
    foo = "Hello"
}

第二種宣告方式:variable declaration

使用時機主要是:

  • 當你不知道變數的起始值
  • 需要在 package scope 宣告變數
  • 當為了程式的閱讀性,將變數組織在一起時

⚠️ 留意:在 package scope 宣告的變數會一直保存在記憶體中,直到程式結束才被釋放,因此應該減少在 package scopes 宣告變數

// 第二種宣告方式,在 main 外面宣告(全域變數),並在 main 內賦值
var foo string
var bar int

// 可以簡寫成
var (
    foo string
    bar int
)

function main() {
  foo = "Hello"
    bar = 100
}

不建議把變數宣告在全域環境

如果變數型別一樣的話,也可以簡寫成這樣:

func main() {
    var c, python, java bool
    fmt.Println(c, python, java)
}

第三種宣告方式

直接宣告並賦值:

// 第三種宣告方式,直接賦值
var (
  foo string = "Hello"
    bar int = 100
)

三種方式是一樣的

下面這兩種寫法是完全一樣的:

var <name> <type> = <value>
var <name> := <value>
// var card string = "Ace of Spades"
card := "Ace of Spades"
// var pi float = 3.14
pi := 3.14

只有在宣告變數的時候可以使用 := 的寫法,如果要重新賦值的話只需要使用 =

注意事項

錯誤:重複宣告變數

// 錯誤:重複宣告變數
paperColor := "Green"
paperColor := "Blue"

正確:我們可以在 main 函式外宣告變數,但無法在 main 函式外賦值

// 正確:我們可以在 main 函式外宣告變數,但無法在 main 函式外賦值
package main

import "fmt"

var deckSize int

func main() {
  deckSize = 50
  fmt.Println(deckSize)
}

錯誤:無法在 main 函式外賦值

// 錯誤:但無法在 main 函式外賦值
package main

import "fmt"

// syntax error: non-declaration statement outside function body
deckSize := 20

func main() {
  fmt.Println(deckSize)
}

錯誤:變數需要先宣告完才能使用

// 錯誤:變數需要先宣告完才能使用
package main

import "fmt"

func main() {
  deckSize = 52            // undefined: deckSize
  fmt.Println(deckSize)    // undefined: deckSize
}

常數(constant)

keywords: iota

使用 :=var 所宣告的會是變數,若需要宣告常數,需要使用 const

const (
    Monday = 1
    Tuesday = 2
    Wednesday = 3
  // ...
)

// 可以簡寫成
// iota 預設從 0 開始,後面的變數自動加一
const (
    Monday = iota + 1
    Tuesday
    Wednesday
  // ...
)

Go 語言裡面定義變數有多種方式。

使用 var 關鍵字是 Go 最基本的定義變數方式,與 C 語言不同的是 Go 把變數型別放在變數名後面:

//定義一個名稱為“variableName”,型別為"type"的變數
var variableName type

定義變數並初始化值

//初始化“variableName”的變數為“value”值,型別是“type”
var variableName type = value

同時初始化多個變數

/*
    定義三個型別都是"type"的變數,並且分別初始化為相應的值
    vname1 為 v1,vname2 為 v2,vname3 為 v3
*/
var vname1, vname2, vname3 type= v1, v2, v3

你是不是覺得上面這樣的定義有點繁瑣?沒關係,因為 Go 語言的設計者也發現了,有一種寫法可以讓它變得簡單一點。我們可以直接忽略型別宣告,那麼上面的程式碼變成這樣了:

/*
    定義三個變數,它們分別初始化為相應的值
    vname1 為 v1,vname2 為 v2,vname3 為 v3
    然後 Go 會根據其相應值的型別來幫你初始化它們
*/
var vname1, vname2, vname3 = v1, v2, v3

現在是不是看上去非常簡潔了?:=這個符號直接取代了 vartype,這種形式叫做簡短宣告。不過它有一個限制,那就是它只能用在函式內部;在函式外部使用則會無法編譯透過,所以一般用 var 方式來定義全域性變數。

/*
    定義三個變數,它們分別初始化為相應的值
    vname1 為 v1,vname2 為 v2,vname3 為 v3
    編譯器會根據初始化的值自動推匯出相應的型別
*/
vname1, vname2, vname3 := v1, v2, v3

常數

所謂常數,也就是在程式編譯階段就確定下來的值,而程式在執行時無法改變該值。在 Go 程式中,常數可定義為數值、布林值或字串等型別。

const constantName = value
//如果需要,也可以明確指定常數的型別:
const Pi float32 = 3.1415926

下面是一些常數宣告的例子:

const Pi = 3.1415926
const i = 10000
const MaxThread = 10
const prefix = "astaxie_"

Go 常數和一般程式語言不同的是,可以指定相當多的小數位數(例如 200 位), 若指定給 float32 自動縮短為 32bit,指定給 float64 自動縮短為 64bit,詳情參考 連結

數值型別

整數型別有無符號和帶符號兩種。Go 同時支援 intuint,這兩種型別的長度相同,但具體長度取決於不同編譯器的實現。Go 裡面也有直接定義好位數的型別:rune, int8, int16, int32, int64byte, uint8, uint16, uint32, uint64。其中 runeint32的別稱,byteuint8 的別稱。

需要注意的一點是,這些型別的變數之間不允許互相賦值或操作,不然會在編譯時引起編譯器報錯。

如下的程式碼會產生錯誤:invalid operation: a + b (mismatched types int8 and int32)

var a int8

var b int32

c:=a + b

另外,儘管 int 的長度是 32 bit, 但 int 與 int32 並不可以互用。

浮點數的型別有 float32float64兩種(沒有 float 型別),預設是float64

這就是全部嗎?No!Go 還支援複數。它的預設型別是complex128(64 位實數+64 位虛數)。如果需要小一些的,也有complex64(32 位實數+32 位虛數)。複數的形式為RE + IMi,其中 RE 是實數部分,IM是虛數部分,而最後的 i 是虛數單位。下面是一個使用複數的例子:

var c complex64 = 5+5i
//output: (5+5i)
fmt.Printf("Value is: %v", c)

字串

我們在上一節中講過,Go 中的字串都是採用UTF-8字符集編碼。字串是用一對雙引號("")或反引號(` )括起來定義,它的型別是string`。

實際在 Go 中,字串是由唯讀的 UTF-8 編碼位元組所組成。

//範例程式碼
var frenchHello string  // 宣告變數為字串的一般方法
var emptyString string = ""  // 宣告了一個字串變數,初始化為空字串
func test() {
    no, yes, maybe := "no", "yes", "maybe"  // 簡短宣告,同時宣告多個變數
    japaneseHello := "Konichiwa"  // 同上
    frenchHello = "Bonjour"  // 常規賦值
}

在 Go 中字串是不可變的,例如下面的程式碼編譯時會報錯:cannot assign to s[0]

var s string = "hello"
s[0] = 'c'

但如果真的想要修改怎麼辦呢?下面的程式碼可以實現:

s := "hello"
c := []byte(s)  // 將字串 s 轉換為 []byte 型別
c[0] = 'c'
s2 := string(c)  // 再轉換回 string 型別
fmt.Printf("%s\n", s2)

Go 中可以使用+運算子來連線兩個字串:

s := "hello,"
m := " world"
a := s + m
fmt.Printf("%s\n", a)

修改字串也可寫為:

s := "hello"
s = "c" + s[1:] // 字串雖不能更改,但可進行切片(slice)操作
fmt.Printf("%s\n", s)

如果要宣告一個多行的字串怎麼辦?可以透過`` `來宣告:

m := `hello
    world`

`` ` 括起的字串為 Raw 字串,即字串在程式碼中的形式就是列印時的形式,它沒有字元轉義,換行也將原樣輸出。例如本例中會輸出:

hello
    world

錯誤型別

Go 內建有一個 error 型別,專門用來處理錯誤資訊,Go 的 package 裡面還專門有一個套件 errors 來處理錯誤:

err := errors.New("emit macho dwarf: elf header corrupted")
if err != nil {
    fmt.Print(err)
}

映射 Map

map有很多種翻譯,名詞叫作地圖,動詞有映射對應對照的意思 ,在一些程式語言中則有Key-Value一個關鍵字對應一個值的用法。

var Variable = map[Type]Type{}

var a = map[int]string{}

可以像這樣bool對應到任何string

var Male = map[bool]string{
	true:  "公",
	false: "母",
}

或是設定string對應到int

var Number = map[string]int{
	"零": 0,
	"壹": 1,
	"貳": 2,
}
Number["參"] = 3

string對應到string也可以,

var Size = map[string]string{
	"big":    "大",
	"medium": "中",
	"small":  "小",
}

只要任兩種型態設定好、對應好之後就能用哩,

string的前後要加雙引號" ",來試試效果吧!

取map

fmt.Print(Male[true],Number["參"])
/* result:
公3
*/

【for 迭代遍歷】

透過for range關鍵字,遍歷造訪結構內的每個元素

for key, value := range Size {
	fmt.Println(key, value)
}
/* result:
big 大
medium 中
small 小
*/

結構 Struct

【struct】

結構裡面可以放多個變數(intstringslicemap等等)、物件、甚至是結構。 宣告結構Struct的幾種方式:

package main

import "fmt"

type Res struct {
	Status string     `json:"status"`
	Msg string        `json:"msg"`
}

func main()  {
	res1 := new(Res)

	var res2 = new(Res)

	var res3 *Res

	res4 := &Res{
		Status: "failed",
	}

	fmt.Println(res1, res2, res3, res4)
	fmt.Printf("%+v %+v %+v %+v",res1, res2, res3, res4)
}

/* result:
&{ } &{ } <nil> &{failed }
&{Status: Msg:} &{Status: Msg:} <nil> &{Status:failed Msg:}
*/

【Nested Structure】巢狀結構

結構中的結構的結構、大腸包小腸再包小小腸

type Wallet struct {
	Blue1000 int // 藍色小朋友
	Red100   int // 紅色國父
	Card     string
}

type PencilBox struct {
	Pencil string
	Pen    string
}

type Bag struct {
	Wallet    // 直接放入結構就好
	PencilBox // 直接放入結構就好
	Books     string
}

type Person struct {
	Bag
	Name string
}

func main() {
	var bag = Bag{
		Wallet{Card: "世華泰國信用無底洞卡", Red100: 5},
		PencilBox{Pen: "Cross", Pencil: "Pentel"},
		"Go繁不及備載", // Books
	}

	var Tommy = Person{}
	Tommy.Name = "Tommy"
	Tommy.Bag = bag

	fmt.Printf("%+v", Tommy)
}

/* result:
{Bag:{Wallet:{Blue1000:0 Red100:5 Card:世華泰國信用無底洞卡} PencilBox:{Pencil:Pentel Pen:Cross} Books:Go繁不及備載} Name:Tommy}
*/

【指標、結構、位址】

這裡將上面的例子取一部分出來修改,

如果將main()裡的var Bag敘述改成 &Bag

type Person struct {
	Bag  // 放Bag這個物件
	Name string
}

func main() {
	var bag = &Bag{
		Wallet{Card: "世華泰國信用無底洞卡", Red100: 5},
		PencilBox{Pen: "Cross", Pencil: "Pentel"},
		"Go繁不及備載",
	}

	var Tommy = Person{}
	Tommy.Name = "Tommy"
	Tommy.Bag = *bag // 透過`取值`來取出bag位址裡面的東西

	fmt.Printf("%+v", Tommy)
}
/* result:
{Bag:{Wallet:{Blue1000:0 Red100:5 Card:世華泰國信用無底洞卡} PencilBox:{Pencil:Pentel Pen:Cross} Books:Go繁不及備載} Name:Tommy}
*/

印出bag 就要透過*來取值


如果將Person裡的Bag改成 *Bag

type Person struct {
	*Bag // 放指標
	Name string
}

func main() {
	var bag = &Bag{ // 指到位址
		Wallet{Card: "世華泰國信用無底洞卡", Red100: 5},
		PencilBox{Pen: "Cross", Pencil: "Pentel"},
		"Go繁不及備載",
	}

	var Tommy = Person{}
	Tommy.Name = "Tommy"
	Tommy.Bag = bag // 這裡就印出bag位址

	fmt.Printf("%+v", Tommy)
}
/* result:
{Bag:0xc000048050 Name:Tommy}
*/

這樣子就會印出bag的位址


雖然有點違反物理法則及常識,但

【小坑】如果Bag裡面有PencilBox,PencilBox裡面又有Bag

會怎麼樣呢? 答案是出現 invalid recursive type 的錯誤。

https://play.golang.org/p/KS5IvIgF1BQ

type PencilBox struct {
	Pencil string
	Pen    string
	Bag           // 你中有我 我中有你
}

type Bag struct {
	Wallet
	PencilBox
	Books     string
}

雖然放物件會出現錯誤,但是 放指針不會

https://play.golang.org/p/mXbp60WDXtR

type PencilBox struct {
	Pencil string
	Pen    string
	*Bag           // 你中有針
}

type Bag struct {
	Wallet
	PencilBox
	Books     string
}

func main() {
	var bag = Bag{
		Wallet{Card: "世華泰國信用無底洞卡", Red100: 5},
		PencilBox{Pen: "Cross", Pencil: "Pentel"},
		"Go繁不及備載",
	}
	bag.PencilBox.Bag = &bag // 包包裡放針

	fmt.Printf("%+v", *bag.PencilBox.Bag)
}

/* result:
{Wallet:{Blue1000:0 Red100:5 Card:世華泰國信用無底洞卡} PencilBox:{Pencil:Pentel Pen:Cross Bag:0xc000046060} Books:Go繁不及備載}
*/

基於結構定義新型態

你可以使用 type 基於 struct 來定義新型態,例如:

package main

import "fmt"

type Point struct {
    X, Y int
}

func main() {
    point1 := Point{10, 20}
    fmt.Println(point1) // {10 20}

    point2 := Point{Y: 20, X: 30}
    fmt.Println(point2) // {30 20}
} 

在上面基於結構定義了新型態 Point,留意到名稱開頭的大小寫,若是大寫的話,就可以在其他套件中存取,這點對於結構的值域也是成立,大寫名稱的值域,才可以在其他套件中存取。在範例中也可以看到,建立並指定結構的值域時,可以直接指定值域名稱,而不一定要按照定義時的順序。

名稱相同的方法

Go 語言中不允許方法重載(Overload),因此,對於以下的程式,是會發生 String 重複宣告的編譯錯誤:

package main

import "fmt"

type Account struct {
    id      string
    name    string
    balance float64
}

func String(account *Account) string {
    return fmt.Sprintf("Account{%s %s %.2f}",
        account.id, account.name, account.balance)
}

type Point struct {
    x, y int
}

func String(point *Point) string { // String redeclared in this block 的編譯錯誤
    return fmt.Sprintf("Point{%d %d}", point.x, point.y)
}

func main() {
    account := &Account{"1234-5678", "Justin Lin", 1000}
    point := &Point{10, 20}
    fmt.Println(account.String())
    fmt.Println(point.String())
}

然而,若是將函式定義為方法,就不會有這個問題,Go 可以從方法的接收者辨別,該使用哪個 String 方法

package main

import "fmt"

type Account struct {
    id      string
    name    string
    balance float64
}

func (ac *Account) String() string {
    return fmt.Sprintf("Account{%s %s %.2f}",
        ac.id, ac.name, ac.balance)
}

type Point struct {
    x, y int
}

func (p *Point) String() string {
    return fmt.Sprintf("Point{%d %d}", p.x, p.y)
}

func main() {
    account := &Account{"1234-5678", "Justin Lin", 1000}
    point := &Point{10, 20}
    fmt.Println(account.String()) // Account{1234-5678 Justin Lin 1000.00}
    fmt.Println(point.String())   // Point{10 20}
}
  • 使用 new syntax:第二種和第三種寫法是一樣的
var user1 *Person       // nil
user2 := &Person{}      // {},user2.firstName 會是 ""
user3 := new(Person)    // {},user3.firstName 會是 ""
// STEP 1:建立一個 person 型別,它本質上是 struct
type Person struct {
    firstName string
    lastName  string
}

// 等同於
type Person struct {
  firstName, lastName string
}

有幾種不同的方式可以根據 struct 來建立變數的:

func main() {
    // 方法一:根據資料輸入的順序決定誰是 firstName 和 lastName
  alex := Person{"Alex", "Anderson"}

  // 直接取得 struct 的 pointer
  alex := &Person{"Alex", "Anderson"}

    // 方法二(建議)
    alex := Person{
        firstName: "Alex",
        lastName:  "Anderson",
    }

  // 方法三:先宣告再賦值
  var alex Person
    alex.firstName = "Alex"
    alex.lastName = "Anderson"
}

定義匿名的 struct(anonymous struct)

也可以不先宣告 struct 直接建立個 struct:

foo := struct {
  Hello string
}{
  Hello: "World",
}

當 pointer 指稱到的是 struct 時

當 pointer 指稱到的是 struct 時,可以直接使用這個 pointer 來對該 struct 進行設值和取值。在 golang 中可以直接使用 pointer 來修改 struct 中的欄位。一般來說,若想要透過 struct pointer(&v)來修改該 struct 中的屬性,需要先解出其值(*p)後使用 (*p).X = 10,但這樣做太麻煩了,因此在 golang 中允許開發者直接使用 p.X 的方式來修改:

type Person struct {
    name string
    age  int32
}

func main() {
    p := &Person{
        name: "Aaron",
    }

  // golang 中允許開發者直接使用 `p.age` 的方式來設值與取值
    p.age = 10  // 原本應該要寫 (*p).X = 10
    fmt.Printf("%+v", p) // {name:Aaron age:10}

另外,使用 struct pointer 時才可以修改到原本的物件,否則會複製一個新的:

func main() {
    r1 := rectangle{"Green"}

    // 複製新的,指稱到不同位置
    r2 := r1
    r2.color = "Pink"
    fmt.Println(r2) // Pink
    fmt.Println(r1) // Green

    // 指稱到相同位置
    r3 := &r1
    r3.color = "Red"
    fmt.Println(r3) // Red
    fmt.Println(r1) // Red
}

建立類別 (Class) 和物件 (Object)

建立物件 (Object)

package main                                  /*  1 */

import (                                      /*  2 */
        "log"                                 /*  3 */
)                                             /*  4 */

// `X` and `Y` are public fields.             /*  5 */
type Point struct {                           /*  6 */
        X float64                             /*  7 */
        Y float64                             /*  8 */
}                                             /*  9 */

// Use an ordinary function as constructor    /* 10 */
func NewPoint(x float64, y float64) *Point {  /* 11 */
        p := new(Point)                       /* 12 */

        p.X = x                               /* 13 */
        p.Y = y                               /* 14 */

        return p                              /* 15 */
}                                             /* 16 */

func main() {                                 /* 17 */
        p := NewPoint(3, 4)                   /* 18 */

        if !(p.X == 3.0) {                    /* 19 */
                log.Fatal("Wrong value")      /* 20 */
        }                                     /* 21 */

        if !(p.Y == 4.0) {                    /* 22 */
                log.Fatal("Wrong value")      /* 23 */
        }                                     /* 24 */
}  

第 6 行至第 9 行的部分是形態宣告。Golang 沿用結構體為類別的型態,而沒有用新的保留字。

第 11 行至第 16 行的部分是建構函式。在一些程式語言中,會有為了建立物件使用特定的建構子 (constructor),而 Golang 沒有引入額外的新語法,直接以一般的函式充當建構函式來建立物件即可。

第 17 行至第 25 行為外部程式。在我們的 Point 物件 p 中,我們直接存取 p 的屬性 XY,這在物件導向上不是好的習慣,因為我們無法控管屬性,物件可能會產生預期外的行為,比較好的方法,是將屬性隱藏在物件內部,由公開方法去存取。我們在後文中會討論。

類別宣告不限定於結構體

雖然大部分的 Golang 類別都使用結構體,但其實 Golang 類別內部可用其他的型別,如下例:

type Vector []float64                     /*  1 */

func NewVector(args ...float64) Vector {  /*  2 */
        return args                       /*  3 */
}                                         /*  4 */

func WithSize(s int) Vector {             /*  5 */
        v := make([]float64, s)           /*  6 */

        return v                          /*  7 */
}      

在第 1 行中,我們宣告 Vector 型態,該型態內部不是使用結構體,而是使用陣列。

我們在第 2 行至第 4 行間及第 5 行至第 8 間宣告了兩個建構函式。由此例可知,Go 不限定建構函式的數量,我們可以視需求使用多個不同的建構函式。

撰寫方法 (Method)

在物件導向程式中,我們很少直接操作屬性 (field),通常會將屬性私有化,再加入相對應的公開方法 (method)。我們將先前的 Point 物件改寫如下:

package main                                  /*  1 */

import (                                      /*  2 */
        "log"                                 /*  3 */
)                                             /*  4 */

// `x` and `y` are private fields.            /*  5 */
type Point struct {                           /*  6 */
        x float64                             /*  7 */
        y float64                             /*  8 */
}                                             /*  9 */

func NewPoint(x float64, y float64) *Point {  /* 10 */
        p := new(Point)                       /* 11 */

        p.SetX(x)                             /* 12 */
        p.SetY(y)                             /* 13 */

        return p                              /* 14 */
}                                             /* 15 */

// The getter of x                            /* 16 */
func (p *Point) X() float64 {                 /* 17 */
        return p.x                            /* 18 */
}                                             /* 19 */

// The getter of y                            /* 20 */
func (p *Point) Y() float64 {                 /* 21 */
        return p.y                            /* 22 */
}                                             /* 23 */

// The setter of x                            /* 24 */
func (p *Point) SetX(x float64) {             /* 25 */
        p.x = x                               /* 26 */
}                                             /* 27 */

// The setter of y                            /* 28 */
func (p *Point) SetY(y float64) {             /* 29 */
        p.y = y                               /* 30 */
}                                             /* 31 */

func main() {                                 /* 32 */
        p := NewPoint(0, 0)                   /* 33 */

        if !(p.X() == 0) {                    /* 34 */
                log.Fatal("Wrong value")      /* 35 */
        }                                     /* 36 */

        if !(p.Y() == 0) {                    /* 37 */
                log.Fatal("Wrong value")      /* 38 */
        }                                     /* 39 */

        p.SetX(3)                             /* 40 */
        p.SetY(4)                             /* 41 */

        if !(p.X() == 3.0) {                  /* 42 */
                log.Fatal("Wrong value")      /* 43 */
        }                                     /* 44 */

        if !(p.Y() == 4.0) {                  /* 45 */
                log.Fatal("Wrong value")      /* 46 */
        }                                     /* 47 */
}  

第 6 行至第 9 行是類別宣告的部分。在這個版本的宣告中,我們將 xy 改為小寫,代表該屬性是私有屬性,其可視度僅限於同一 package 中。

第 10 行至第 15 行是 Point 類別的建構函式。請注意我們刻意在第 12 行及第 13 行用該類別的 setters 來初始化屬性,這是刻意的動作。因為我們要確保在設置屬性時的行為保持一致。

第 16 行至第 31 行是 Point 類別的 getters 和 setters。所謂的 getters 和 setters 是用來存取內部屬性的 method。比起直接暴露屬性,使用 getters 和 setters 會有比較好的控制權。日後要修改 getters 或 setters 的實作時,也只要修改同一個地方即可。

在本例中,getters 和 setters 都是公開 method。但 getters 或 setters 不一定必為公開 method。例如,我們想做唯讀的 Point 物件時,就可以把 setters 的部分設為私有 method,留給類別內部使用。

在 Go 語言中,沒有 thisself 這種代表物件的關鍵字,而是由程式設計者自訂代表物件的變數,在本例中,我們用 p 表示物件本身。透過這種帶有物件的函式宣告後,函式會和物件連動;在物件導向中,將這種和物件連動的函式稱為方法 (method)。

雖然在這個例子中,暫時無法直接看出使用方法的好處,比起直接操作屬性,透過私有屬性搭配公開方法帶來許多的益處。例如,如果我們希望 Point 在建立之後是唯讀的,我們只要將 SetXSetY 改為私有方法即可。或者,我們希望限定 Point 所在的範圍為 0.0 至 1000.0,我們可以在 SetXSetY 中檢查參數是否符合我們的要求。

靜態方法 (Static Method)

有些讀者學過 Java 或 C#,可能有聽過過靜態方法 (static method)。這是因為 Java 和 C# 直接將物件導向的概念融入其語法中,然而,為了要讓某些方法在不建立物件時即可使用,所使用的一種補償性的語法機制。由於 Go 語言沒有將物件導向的概念直接加在語法中,不需要用這種語法,直接用頂層函式即可。

例如:我們撰寫一個計算兩點間長度的函式:

package main                                   /*  1 */

import (                                       /*  2 */
        "log"                                  /*  3 */
        "math"                                 /*  4 */
)                                              /*  5 */

type Point struct {                            /*  6 */
        x float64                              /*  7 */
        y float64                              /*  8 */
}                                              /*  9 */

func NewPoint(x float64, y float64) *Point {   /* 10 */
        p := new(Point)                        /* 11 */

        p.SetX(x)                              /* 12 */
        p.SetY(y)                              /* 13 */

        return p                               /* 14 */
}                                              /* 15 */

func (p *Point) X() float64 {                  /* 16 */
        return p.x                             /* 17 */
}                                              /* 18 */

func (p *Point) Y() float64 {                  /* 19 */
        return p.y                             /* 20 */
}                                              /* 21 */

func (p *Point) SetX(x float64) {              /* 22 */
        p.x = x                                /* 23 */
}                                              /* 24 */

func (p *Point) SetY(y float64) {              /* 25 */
        p.y = y                                /* 26 */
}                                              /* 27 */

// Use an ordinary function as static method.  /* 28 */
func Dist(p1 *Point, p2 *Point) float64 {      /* 29 */
        xSqr := math.Pow(p1.X()-p2.X(), 2)     /* 30 */
        ySqr := math.Pow(p1.Y()-p2.Y(), 2)     /* 31 */

        return math.Sqrt(xSqr + ySqr)          /* 32 */
}                                              /* 33 */

func main() {                                  /* 34 */
        p1 := NewPoint(0, 0)                   /* 35 */
        p2 := NewPoint(3.0, 4.0)               /* 36 */

        if !(Dist(p1, p2) == 5.0) {            /* 37 */
                log.Fatal("Wrong value")       /* 38 */
        }                                      /* 39 */
}   

本範例和前一節的範例大同小異。主要的差別在於第 29 行至第 33 間多了一個用來計算距離的函式。該函式不綁定特定的物件,相當於 Java 的靜態函式。

因為 Golang 不是 Java 這種純物件導向語言,而是混合命令式和物件式兩種語法,所以不需要使用特定的語法來實踐靜態函式,使用一般的函式即可。

或許有讀者會擔心,使用過多的頂層函式會造成全域空間的汙染和衝突;實際上不需擔心,雖然我們目前將物件和主程式寫在一起,實務上,物件會寫在獨立的package 中,藉由 package 即可大幅減低命名空間衝突的議題。

使用嵌入 (Embedding) 取代繼承 (Inheritance)

繼承 (inheritance) 是一種重用程式碼的方式,透過從父類別 (parent class) 繼承程式碼,子類別 (child class) 可以少寫一些程式碼。此外,對於靜態型別語言來說,繼承也是實現多型 (polymorphism) 的方式。然而,Go 語言卻刻意地拿掉繼承,這是出自於其他語言的經驗。

繼承雖然好用,但也引起許多的問題。像是 C++ 相對自由,可以直接使用多重繼承,但這項特性會引來菱型繼承 (diamond inheritance) 的議題,Java 和 C# 刻意把這個機制去掉,改以介面 (interface) 進行有限制的多重繼承。從過往經驗可知過度地使用繼承,會增加程式碼的複雜度,使得專案難以維護。出自於工程上的考量,Go 捨去繼承這個語法特性。

為了補償沒有繼承的缺失,Go 加入了嵌入 (embedding) 這個新的語法特性,透過嵌入,也可以達到程式碼共享的功能。

例如,我們擴展 Point 類別至三維空間:

package main                                                 /*  1 */

import (                                                     /*  2 */
        "log"                                                /*  3 */
)                                                            /*  4 */

type Point struct {                                          /*  5 */
        x float64                                            /*  6 */
        y float64                                            /*  7 */
}                                                            /*  8 */

func NewPoint(x float64, y float64) *Point {                 /*  9 */
        p := new(Point)                                      /* 10 */

        p.SetX(x)                                            /* 11 */
        p.SetY(y)                                            /* 12 */

        return p                                             /* 13 */
}                                                            /* 14 */

func (p *Point) X() float64 {                                /* 15 */
        return p.x                                           /* 16 */
}                                                            /* 17 */

func (p *Point) Y() float64 {                                /* 18 */
        return p.y                                           /* 19 */
}                                                            /* 20 */

func (p *Point) SetX(x float64) {                            /* 21 */
        p.x = x                                              /* 22 */
}                                                            /* 23 */

func (p *Point) SetY(y float64) {                            /* 24 */
        p.y = y                                              /* 25 */
}                                                            /* 26 */

type Point3D struct {                                        /* 27 */
        // Point is embedded                                 /* 28 */
        Point                                                /* 29 */
        z float64                                            /* 30 */
}                                                            /* 31 */

func NewPoint3D(x float64, y float64, z float64) *Point3D {  /* 32 */
        p := new(Point3D)                                    /* 33 */

        p.SetX(x)                                            /* 34 */
        p.SetY(y)                                            /* 35 */
        p.SetZ(z)                                            /* 36 */

        return p                                             /* 37 */
}                                                            /* 38 */

func (p *Point3D) Z() float64 {                              /* 39 */
        return p.z                                           /* 40 */
}                                                            /* 41 */

func (p *Point3D) SetZ(z float64) {                          /* 42 */
        p.z = z                                              /* 43 */
}                                                            /* 44 */

func main() {                                                /* 45 */
        p := NewPoint3D(1, 2, 3)                             /* 46 */

        // GetX method is from Point                         /* 47 */
        if !(p.X() == 1) {                                   /* 48 */
                log.Fatal("Wrong value")                     /* 49 */
        }                                                    /* 50 */

        // GetY method is from Point                         /* 51 */
        if !(p.Y() == 2) {                                   /* 52 */
                log.Fatal("Wrong value")                     /* 53 */
        }                                                    /* 54 */

        // GetZ method is from Point3D                       /* 55 */
        if !(p.Z() == 3) {                                   /* 56 */
                log.Fatal("Wrong value")                     /* 57 */
        }                                                    /* 58 */
}     

第 5 行至第 26 行是原本的 Point 類別,這和先前的實作是雷同的,不多做說明。

第 27 行至第 44 行是 Point3D 類別,我們來看一下這個類別。

第 27 行至第 31 行是 Point3D 的類別宣告。請注意我們在第 29 行嵌入了 Point 類別。

第 32 行至第 38 行是 Point3d 的建構函式。雖然我們沒有為 Point3D 宣告 SetX()SetY() method,但我們有嵌入 Point 類別,所以我們在第 34 行及第 35 行可以直接使用這些 method。

第 45 行至第 59 行是外部程式的部分。由於我們的 Point3D 內嵌了 Point,雖然 Point3D 沒有自己實作 X()Y() method,我們在第 48 行及第 52 行可直接呼叫這些 method。

在本例中,我們重用了 Point 的方法,再加入 Point3D 特有的方法。實際上的效果等同於繼承。

然而,PointPoint3D 兩者在類別關係上卻是不相干的獨立物件。在以下例子中,我們想將 Point3D 加入 Point 物件組成的切片,而引發程式的錯誤:

// Declare Point and Point3D as above.
 
func main() {
    points := make([]*Point, 0)
 
    p1 := NewPoint(3, 4)
    p2 := NewPoint3D(1, 2, 3)
 
    // Error!
    points = append(points, p1, p2)
}

在 Go 語言中,需要使用介面 (interface) 來解決這個議題,這就是我們下一篇文章所要探討的主題。

嵌入指標

除了嵌入其他結構外,結構也可以嵌入指標。我們將上例改寫如下:

package main
 
import (
    "log"
)
 
type Point struct {
    x float64
    y float64
}
 
func NewPoint(x float64, y float64) *Point {
    p := new(Point)
 
    p.SetX(x)
    p.SetY(y)
 
    return p
}
 
func (p *Point) X() float64 {
    return p.x
}
 
func (p *Point) Y() float64 {
    return p.y
}
 
func (p *Point) SetX(x float64) {
    p.x = x
}
 
func (p *Point) SetY(y float64) {
    p.y = y
}
 
type Point3D struct {
    // Point is embedded as a pointer
    *Point
    z float64
}
 
func NewPoint3D(x float64, y float64, z float64) *Point3D {
    p := new(Point3D)
 
    // Forward promotion
    p.Point = NewPoint(x, y)
 
    // Forward promotion
    p.Point.SetX(x)
    p.Point.SetY(y)
 
    p.SetZ(z)
 
    return p
}
 
func (p *Point3D) Z() float64 {
    return p.z
}
 
func (p *Point3D) SetZ(z float64) {
    p.z = z
}
 
func main() {
    p := NewPoint3D(1, 2, 3)
 
    // GetX method is from Point
    if !(p.X() == 1) {
        log.Fatal("Wrong value")
    }
 
    // GetY method is from Point
    if !(p.Y() == 2) {
        log.Fatal("Wrong value")
    }
 
    // GetZ method is from Point3D
    if !(p.Z() == 3) {
        log.Fatal("Wrong value")
    }
}

同樣地,仍然不能透過嵌入指楆讓型別直接互通,而需要透過介面 (interface)。

Array和Slice的區別

Array

Go語言中的Array即為資料的一種集合,需要在宣告時指定容量和初值,且一旦宣告就長度固定了,訪問時按照索引進行訪問。通過內建函式len可以獲取陣列中的元素個數。

陣列在初始化時必須指定大小和初值,不過Go語言為我們提供了一些更為靈活的方式進行初始化。例如:使用...來自動獲取長度;未指定值時,用0賦予初值;指定指定元素的初值等。下面給出一些陣列初始化的方式示例。

var arr [5]int	//聲明瞭一個大小為5的陣列,預設初始化值為[0,0,0,0,0]
arr := [5]int{1}	//宣告並初始化了一個大小為5的陣列的第一個元素,初始化後值為[1,0,0,0,0]
arr := [...]int{1,2,3}	//通過...自動獲取陣列長度,根據初始化的值的數量將大小初始化為3,初始化後值為[1,2,3]
arr := [...]int{4:1}	//指定序號為4的元素的值為1,通過...自動獲取長度為5,初始化後值為[0,0,0,0,1]

函式引數

Go語言陣列作為函式引數時,必須指定引數陣列的大小,且傳入的陣列大小必須與指定的大小一致,陣列為按值傳遞的,函式內對陣列的值的改變不影響初始陣列:

package main

import "fmt"

//PrintArray print the value of array
func PrintArray(arr [5]int) {
    arr[0] = 5
	fmt.Println(arr)
}

func main() {
	a := [...]int{4:1}
	PrintArray(a)	// [5,0,0,0,1]
    fmt.Println(a)	// [0,0,0,0,1]
}

Slice

切片是Go語言中極為重要的一種資料型別,可以理解為動態長度的陣列(雖然實際上Slice結構內包含了一個數組),訪問時可以按照陣列的方式訪問,也可以通過切片操作訪問。Slice有三個屬性:指標、長度和容量。指標即Slice名,指向的為陣列中第一個可以由Slice訪問的元素;長度指當前slice中的元素個數,不能超過slice的容量;容量為slice能包含的最大元素數量,但實際上當容量不足時,會自動擴充為原來的兩倍。通過內建函式lencap可以獲取slice的長度和容量。

Slice在初始化時需要初始化指標,長度和容量,容量未指定時將自動初始化為長度的大小。可以通過直接獲取陣列的引用、獲取陣列/slice的切片構建或是make函式初始化陣列。下面給出一些slice初始化的方式示例。

s := []int{1,2,3}	//通過陣列的引用初始化,值為[1,2,3],長度和容量為3

arr := [5]int{1,2,3,4,5}
s := arr[0:3]	//通過陣列的切片初始化,值為[1,2,3],長度為3,容量為5

s := make([]int, 3)	//通過make函式初始化,值為[0,0,0],長度和容量為3

s := make([]int, 3, 5)	//通過make函式初始化,值為[0,0,0],長度為3,容量為5

其中特別需要注意的是通過切片方式初始化。若是通過對slice的切片進行初始化,實際上初始化之後的結構如下圖所示:

此時x的值為[2,3,5,7,11],y的值為[3,5,7],且兩個slice的指標指向的是同一個陣列,也即x中的元素的值的改變將會導致y中的值也一起改變。

這樣的初始化方式可能會導致記憶體被過度佔用,如只需要使用一個極大的陣列中的幾個元素,但是由於需要指向整個陣列,所以整個陣列在GC時都無法被釋放,一直佔用記憶體空間。故使用切片操作進行初始化時,最好使用append函式將切片出來的資料複製到一個新的slice中,從而避免記憶體佔用陷阱。

函式引數

Go語言Slice作為函式引數傳遞時為按引用傳遞的,函式內對slice內元素的修改將導致函式外的值也發生改變,不過由於傳入函式的是一個指標的副本,所以對該指標的修改不會導致原來的指標的變化(例如append函式不會改變原來的slice的值)。具體可以根據下面的程式碼進行理解:

package main

import "fmt"

//PrintSlice print the value of slice
func PrintSlice(s []int) {
	s = append(s, 4)
	s[0] = -1
	fmt.Println(s)
}

func main() {
	s := []int{1,2,3,4,5}
	s1 := s[0:3]

    fmt.Println("s:",s)	//s: [1,2,3,4,5]
    fmt.Println("s1:",s1)	//s1: [1,2,3]
	PrintSlice(s1)	//[-1,2,3,4]
	fmt.Println("s:",s)	//[-1,2,3,4,5]
	fmt.Println("s1:",s1)	//[-1,2,3]
}

總結

  • 陣列長度不能改變,初始化後長度就是固定的;切片的長度是不固定的,可以追加元素,在追加時可能使切片的容量增大。
  • 結構不同,陣列是一串固定資料,切片描述的是擷取陣列的一部分資料,從概念上說是一個結構體。
  • 初始化方式不同,如上。另外在宣告時的時候:宣告陣列時,方括號內寫明瞭陣列的長度或使用...自動計算長度,而宣告slice時,方括號內沒有任何字元。
  • unsafe.sizeof的取值不同,unsafe.sizeof(slice)返回的大小是切片的描述符,不管slice裡的元素有多少,返回的資料都是24位元組。unsafe.sizeof(arr)的值是在隨著arr的元素的個數的增加而增加,是陣列所儲存的資料記憶體的大小。
  • 函式呼叫時的傳遞方式不同,陣列按值傳遞,slice按引用傳遞。

指標

Go 語言中有指標(Pointer),你可以在宣告變數時於型態前加上 *,這表示建立一個指標,例如:

var i *int

這時 i 是個空指標,也就是值為 nil,上頭等同於 var i *int = nil,目前並沒有儲存任何位址,如果想讓它儲存另一個變數的記憶體位址,可以使用 & 取得變數位址並指定給 i,例如:

package main

import "fmt"

func main() {
    var i *int
    j := 1

    i = &j
    fmt.Println(i)  // 0x104382e0 之類的值
    fmt.Println(*i) // 1

    j = 10
    fmt.Println(*i) // 10

    *i = 20
    fmt.Println(j) // 20
}

j 的位置儲存了 1,那麼具體來說,j 的位置到底是在哪?這就是 & 取址運算的目的,&j 具體取得了 j 的位置,然後指定給 i

如上所示,如果想存取指標位址處的變數儲存的值,可以使用 *,因而,你改變 j 的值,*i 取得的就是改變後的值,透過 *i 改變值,從 j 取得的也會是改變後的值。

package main

import "fmt"

func add1To(n *int) {
	*n = *n + 1
}

func main() {
	number := 1
	add1To(&number)
	fmt.Println(number) // 2
}

打印型態

  • 使用reflect的TypeOf方法
  • 使用Printf中的 %T
package main

import (
	"fmt"
	"reflect"
)

func main() {
	x := 10
	p := &x
	fmt.Printf("%T\n", p)
	fmt.Println(reflect.TypeOf(p))

}

結構與指標

如果你建立了一個結構的實例,並將之指定給另一個結構變數,那麼會進行值域的複製。例如:

package main

import "fmt"

type Point struct {
    X, Y int
}

func main() {  
    point1 := Point{X: 10, Y: 20}
    point2 := point1

    point1.X = 20

    fmt.Println(point1)  // {20, 20}
    fmt.Println(point2)  // {10 20}
}

這對於函式的參數傳遞也是一樣的:

package main

import "fmt"

type Point struct {
    X, Y int
}

func changeX(point Point) {
    point.X = 20
    fmt.Println(point)
}

func main() {
    point := Point{X: 10, Y: 20}

    changeX(point)     // {20 20}
    fmt.Println(point) // {10 20}
}

point 的位置開始儲存了結構,可以對 point 使用 & 取值,將位址值指定給指標,因此若指定或傳遞結構時,不是想要複製值域,可以使用指標。例如:

package main

import "fmt"

type Point struct {
    X, Y int
}

func main() {
    point1 := Point{X: 10, Y: 20}
    point2 := &point1

    point1.X = 20

    fmt.Println(point1) // {20, 20}
    fmt.Println(point2) // &{20 20}
}

注意到 point2 := &point1 多了個 &,這取得了 point1 實例的指標值,並傳遞給 point2point2 的型態是 *Point,也就是相當於 var point2 *Point = &point1,因此,當你透過 point1.X 改變了值,透過 point2 就能取得對應的改變。

類似地,也可以在傳遞參數給函式時使用指標:

package main

import "fmt"

type Point struct {
    X, Y int
}

func changeX(point *Point) {
    point.X = 20
    fmt.Printf("&{%d %d}\n", point.X, point.Y)
}

func main() {
    point := Point{X: 10, Y: 20}

    changeX(&point)    // &{20 20}
    fmt.Println(point) // {20 20}
}

可以看到在 Go 語言中,即使是指標,也可以直接透過點運算子來存取值域,這是 Go 提供的語法糖,point.X 在編譯過後,會被轉換為 (*point).X

你也可以透過 new 來建立結構實例,這會傳回結構實例的位址:

package main

import "fmt"

type Point struct {
    X, Y int
}

func default_point() *Point {
    point := new(Point)
    point.X = 10
    point.Y = 10
    return point
}

func main() {
    point := default_point()
    fmt.Println(point) // &{10 10}
}

在這邊,point 是個指標,也就是 *Point 型態,儲存了結構實例的位址。

結構的值域也可以是指標型態,也可以是結構自身型態之指標,因此可實現鏈狀參考,例如:

package main

import "fmt"

type Point struct {
    X, Y int
}

type Node struct {
    point *Point
    next  *Node
}

func main() {
    node := new(Node)

    node.point = &Point{10, 20}
    node.next = new(Node)

    node.next.point = &Point{10, 30}

    fmt.Println(node.point)      // &{10 20}
    fmt.Println(node.next.point) // &{10 30}
}

$T{} 的寫法與 new(T) 是等效的,使用 &Point{10, 20} 這類的寫法,可以同時指定結構的值域。

Json

go 的 json.Unmarshal 可以把 json 字串轉成 struct,而 json.Marshal 可以將 struct 轉成 json 字串.

package main

import (
	"encoding/json"
	"fmt"
)

type Person struct {
	Id   int    `json:"id"`
	Name string `json:name`
}

func main() {
	data := []byte(`{"id" : 1 , "name" : "Daniel"}`)
	var person Person
	json.Unmarshal(data, &person)
	fmt.Println(person)
	jsondata, _ := json.Marshal(person)
	fmt.Println(string(jsondata))
}

協程同步的三個方法

Mutex

互斥鎖,可以創建為其他結構體的字段;零值為解鎖 狀態,Mutex類型的鎖和線程無關,可以由不同的線程加鎖和解鎖。

Channel

使用Go語言的channel

WaitGroup

它能夠阻塞主線程的執行,直到所有的goroutine執行完畢。要注意goroutine的執行結果是亂序的,調度器無法保証goroutine執行順序,且進程結束時不會等待goroutine退出。

WaitGroup使用詳解

WaitGroup總共有三個方法:

  • Add(delta int) : 計數器增加delta
  • Done() : 計數器-1,相當於Add(-1)
  • Wait() : 阻塞直到所有的WaitGroup數量變為零,即計數器變為0

sync.WaitGroup實現了一個類似Que的資料結構,我們可以不斷地向Que添加並發任務,每添加一個任務,就將計數器的值增加1,若我們啟動了 N 個並發任務時時,就需要把計數器增加 N 。每個任務完成時通過呼叫 Done()方法將計數器減1,並且從Que中刪除。如果隊例中的任務尚未執行完畢,我們通過調用 Wait() 來發出阻塞, 直到計數器歸零時,表示所有並發協程已經完成。

var wg sync.WaitGroup //宣告全域的WaitGroup
var count int32

func AddOne() { //定義函數,每次調用時count加1
	defer wg.Done()
	count++
}

func main()  {
	wg.Add(3) //往WaitGroup裡添加3個goroutine
	go AddOne()
	go AddOne()
	go AddOne()
	wg.Wait()
	fmt.Printf("Count: %d", count ) //執行結束,輸出Count: 3
}

WaitGroup的特點是可以調用Wait()來阻塞隊列,直到隊列中的並發任務執行完畢才解除阻塞,不用sleep固定時間來等待。缺點是無法指定goroutine的並發協程數目。

WaitGroup源碼閱讀

信號量

信號量是Unix系統提供的一種共享資源的保護機制,用於防止多個線程同時訪問某個資源。

當信號量>0時,表示資源可用。 當信號量==0時,表示資源暫不可用。

線程獲取資源時,系統將信號量減1。當信號量為0時,當前線程會進入睡眠,直到信號量為正時線程會被喚醒。

資料結構

源碼包src/sync/waitgroup.go:WaitGroup的結構體定義如下:

type WaitGroup struct {
	state1 [3]uint32
}

state1 是一個長度為3的array,包含了兩組計數器和一個信號量。

  • counter : 當前還未執行結束的goroutine計數器
  • waiter count : 等待goroutine-group結束的goroutine數量,即等候者的數量
  • semaphore : 信號量

WaitGroup對外提供三個接口,Add(delta int),Wait()和Done(),下面介紹這三個函數的實現細節。

Add(delta int)

Add()的功能有兩個,第一個是將delta值加到counter裡頭,因為delta可以為負值,所以counter有可能變成0或負值。Add()的第二個功能就是判斷counter的值,當其為0時,根據 waiter 數值釋放等量的信號量,把等待的goroutine全部喚醒,如果counter變為負值,則panic。

func (wg *WaitGroup) Add(delta int) {
    statep, semap := wg.state() //獲取state和semphore的指針
    
    state := atomic.AddUint64(statep, uint64(delta)<<32) //把delta值加到counter
    v := int32(state >> 32) //獲取counter值
    w := uint32(state)      //獲取waiter值
    
    if v < 0 {              //如果counter值為負數,則panic
        panic("sync: negative WaitGroup counter")
    }

    //如果counter大於零,或是waiter為零(沒有等待者),則直接退出
    if v > 0 || w == 0 {
        return
    }

    //當counter等於0時,waiter一定大於零(內部維護waiter數目,不會出現小於等於零的情況)
    //先把counter歸零,再釋放waiter個數的信號量
    *statep = 0
    for ; w != 0; w-- {
        runtime_Semrelease(semap, false)
    }
}

Wait()

Wait()的功能為累加waiter以及阻塞等待信號量

func (wg *WaitGroup) Wait() {
    statep, semap := wg.state() //獲取state和semaphore的指針
    for {
        state := atomic.LoadUint64(statep) //獲取state值
        v := int32(state >> 32)            //獲取counter值
        w := uint32(state)                 //獲取waiter值
        if v == 0 {                        //當counter為0,代表所有的goroutine都結束了,直接退出
            return
        }
        
        // 使用CAS函數累加waiter,保証有多個goroutine同時執行Wait()時也能正確累加waiter
        if atomic.CompareAndSwapUint64(statep, state, state+1) {
            runtime_Semacquire(semap) 
            return
        }
    }
}

Done()

Done()等同於Add(-1),也就是把counter減1。

func (wg *WaitGroup) Done() {
	wg.Add(-1)
}

WaitGroup的坑

  • Add()操作必須早於Wait(),否則會panic

  • Add()設置的值必須與實際等待的goroutine數量一致,否則會panic

defer、panic、recover

defer 延遲執行

在 Go 語言中,可以使用 defer 指定某個函式延遲執行,那麼延遲到哪個時機?簡單來說,在函式 return 之前,例如:

package main

import "fmt"

func deferredFunc() {
    fmt.Println("deferredFunc")    
}

func main() {
    defer deferredFunc()
    fmt.Println("Hello, 世界")    
}

這個範例執行時,deferredFunc() 前加上了 defer,因此,會在 main() 函式 return 前執行,結果就是先顯示了 "Hello, 世界",才顯示 "deferredFunc"

如果有多個函式被 defer,那麼在函式 return 前,會依 defer 的相反順序執行,也就是 LIFO,例如:

package main

import "fmt"

func deferredFunc1() {
    fmt.Println("deferredFunc1")
}

func deferredFunc2() {
    fmt.Println("deferredFunc2")
}

func main() {
    defer deferredFunc1()
    defer deferredFunc2()
    fmt.Println("Hello, 世界")
}

由於先 deferdeferredFunc1(),才 deferdeferredFunc2(),因此執行結果會是 "Hello, 世界""deferredFunc2""deferredFunc1" 的顯示順序。

使用 defer 清除資源

package main

import (
    "fmt"
    "os"
)

func main() {
    f, err := os.Open("/tmp/dat")
    if err != nil {
        fmt.Println(err)
        return;
    }

    defer func() { // 延遲執行,而且函式 return 前一定會執行
        if f != nil {
            f.Close()
        }
    }()

    b1 := make([]byte, 5)
    n1, err := f.Read(b1)
    if err != nil {
        fmt.Printf("%d bytes: %s\n", n1, string(b1))
        // 處理讀取的內容....
    }
}

這麼一來,若 Read 發生錯誤,最後一定會執行被 defer 的函式,從而保證了 f.Close() 一定會關閉檔案。

(就某些意義來說,defer 的角色類似於例外處理機制中 finally 的機制,將資源清除的函式,藉由 defer 來處理,一方面大概也是為了在程式碼閱讀上,強調出資源清除的重要性吧!)

panic 恐慌中斷

方才稍微提過,如果在函式中執行 panic,那麼函式的流程就會中斷,若 A 函式呼叫了 B 函式,而 B 函式中呼叫了 panic,那麼 B 函式會從呼叫了 panic 的地方中斷,而 A 函式也會從呼叫了 B 函式的地方中斷,若有更深層的呼叫鏈,panic 的效應也會一路往回傳播。

(如果你有例外處理的經驗,這就相當於被拋出的例外都沒有處理的情況。)

可以將方才的範例改寫為以下:

package main

import (
    "fmt"
    "os"
)

func check(err error) {
    if err != nil {
        panic(err)
    }
}

func main() {
    f, err := os.Open("/tmp/dat")
    check(err)

    defer func() {
        if f != nil {
            f.Close()
        }
    }()

    b1 := make([]byte, 5)
    n1, err := f.Read(b1)
    check(err)

    fmt.Printf("%d bytes: %s\n", n1, string(b1))
}

如果在開啟檔案時,就發生了錯誤,假設這是在一個很深的呼叫層次中發生,若你直接想撰寫程式,將 os.Openerror 逐層傳回,那會是一件很麻煩的事,此時直接發出 panic,就可以達到想要的目的。

recover 恢復流程

如果發生了 panic,而你必須做一些處理,可以使用 recover,這個函式必須在被 defer 的函式中執行才有效果,若在被 defer 的函式外執行,recover 一定是傳回 nil

如果有設置 defer 函式,在發生了 panic 的情況下,被 defer 的函式一定會被執行,若當中執行了 recover,那麼 panic 就會被捕捉並作為 recover 的傳回值,那麼 panic 就不會一路往回傳播,除非你又呼叫了 panic

因此,雖然 Go 語言中沒有例外處理機制,也可使用 deferpanicrecover 來進行類似的錯誤處理。例如,將上頭的範例,再修改為:

package main

import (
    "fmt"
    "os"
)

func check(err error) {
    if err != nil {
        panic(err)
    }
}

func main() {
    f, err := os.Open("/tmp/dat")
    check(err)

    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err) // 這已經是頂層的 UI 介面了,想以自己的方式呈現錯誤
        }

        if f != nil {
            if err := f.Close(); err != nil {
                panic(err) // 示範再拋出 panic
            }
        }
    }()

    b1 := make([]byte, 5)
    n1, err := f.Read(b1)
    check(err)

    fmt.Printf("%d bytes: %s\n", n1, string(b1))
}

在這個例子中,假設已經是最頂層的 UI 介面了,因此使用 recover 嘗試捕捉 panic,並以自己的方式呈現錯誤,附帶一題的是,關閉檔案也有可能發生錯誤,程式中也檢查了 f.Close(),視需求而定,你可以像這邊重新拋出 panic,或者也可以單純地設計一個 UI 介面來呈現錯誤。

什麼時候該用 error?什麼時候該用 panic?在 Go 的慣例中,鼓勵你使用 error,明確地進行錯誤檢查,然而,就如方才所言,巢狀且深層的呼叫時,使用 panic 會比較便於傳播錯誤,就 Go 的慣例來說,是以套件為界限,於套件之中,必要時可以使用 panic,而套件公開的函式,建議以 error 來回報錯誤,若套件公開的函式可能會收到 panic,建議使用 recover 捕捉,並轉換為 error

結構與方法

建立方法

假設可能原本有如下的程式內容,負責銀行帳戶的建立、存款與提款:

package main

import (
    "errors"
    "fmt"
)

type Account struct {
    id      string
    name    string
    balance float64
}

func Deposit(account *Account, amount float64) {
    if amount <= 0 {
        panic("必須存入正數")
    }
    account.balance += amount
}

func Withdraw(account *Account, amount float64) error {
    if amount > account.balance {
        return errors.New("餘額不足")
    }
    account.balance -= amount
    return nil
}

func String(account *Account) string {
    return fmt.Sprintf("Account{%s %s %.2f}",
        account.id, account.name, account.balance)
}

func main() {
    account := &Account{"1234-5678", "Justin Lin", 1000}
    Deposit(account, 500)
    Withdraw(account, 200)
    fmt.Println(String(account)) // Account{1234-5678 Justin Lin 1300.00}
}

實際上,DespositWithdrawString 的函式操作,都是與傳入的 Account 實例有關,何不將它們組織在一起呢?這樣比較容易使用些,在 Go 語言中,你可以重新修改函式如下:

package main

import (
    "errors"
    "fmt"
)

type Account struct {
    id      string
    name    string
    balance float64
}

func (ac *Account) Deposit(amount float64) {
    if amount <= 0 {
        panic("必須存入正數")
    }
    ac.balance += amount
}

func (ac *Account) Withdraw(amount float64) error {
    if amount > ac.balance {
        return errors.New("餘額不足")
    }
    ac.balance -= amount
    return nil
}

func (ac *Account) String() string {
    return fmt.Sprintf("Account{%s %s %.2f}",
        ac.id, ac.name, ac.balance)
}

func main() {
    account := &Account{"1234-5678", "Justin Lin", 1000}
    account.Deposit(500)
    account.Withdraw(200)
    fmt.Println(account.String()) // Account{1234-5678 Justin Lin 1300.00}
}

簡單來說,只是將函式的第一個參數,移至方法名稱之前成為函式呼叫的接收者(Receiver),這麼一來,就可以使用 account.Deposit(500)account.Withdraw(200)account.String() 這樣的方式來呼叫函式,就像是物件導向程式語言中的方法(Method)。

注意到,在這邊使用的是 (ac *Account),也就是指標,如果你是如下使用 (ac Account)

func (ac Account) Deposit(amount float64) {
    if amount <= 0 {
        panic("必須存入正數")
    }
    ac.balance += amount
}

那麼執行像是 account.Deposit(500),就像是以 Deposit(*account, 500) 呼叫以下函式:

func Deposit(account Account, amount float64) {
    if amount <= 0 {
        panic("必須存入正數")
    }
    account.balance += amount
}

也就是,相當於將 Account 實例以傳值方式複製給 Deposit 函式的參數。

某些程度上,可以將接收者想成是其他語言中的 thisselfGo 建議為接收者適當命名,而不是用 thisself 之類的名稱。接收者並沒有文件上記載的作用,命名時不用其他參數具有一定的描述性,只要能表達程式意圖就可以了,Go 建議是個一或兩個字母的名稱(某些程度上,也可以用來與其他參數區別)。

Channel

package main

import (
	"fmt"
)

func main() {
	var test = make(chan int)
	go func() { test <- 123 }() // 如果傳遞值到 channel 時不在 go func 內程式會卡住
	msg := <-test               // channel 是一個地址,要賦予給一個變數後才能讀出
	fmt.Println(test)
	fmt.Println(msg)
}

go-gorilla的ping pong

業務需求,ping每隔60秒執行一次,ping兩次後,沒有得到pong的消息,自動切斷client。

pongTime=180 * time.Second
pingTime=60 * time.Second

readPump()
c.conn.SetReadDeadline(time.Now().Add(pongTime))
	c.conn.SetPongHandler(func(string) error {
		c.conn.SetReadDeadline(time.Now().Add(pongTime))
		return nil
	})

readPump()
ticker := time.NewTicker(pingTime)
c.conn.WriteMessage(websocket.PingMessage, []byte{})

關於ping/pong,一般瀏覽器接收到ping之後會自動返回pong. 但是用nodejs,go等編寫的客戶端,可能會需要明文編寫 pong返回信息, 這個需要根據自己的環境是否支持自動返信。 因為我用nodejs做的客戶端接收到ping以後沒有明文返回pong消息,但是在服務器端可以自動接收到pong的消息。

new 跟 make 使用時機

https://blog.wu-boy.com/2021/06/what-is-different-between-new-and-make-in-golang/

大家接觸 Go 語言肯定對 newmake 不陌生,但是什麼時候要使用 new 什麼時候用 make,也許是很多剛入門的開發者比較不懂,本篇就簡單筆記 newmake 的差異及使用時機。

使用 new 關鍵字

Go 提供兩種方式來分配記憶體,一個是 new 另一個是 make,這兩個關鍵字做的事情不同,應用的類型也不同,可能會造成剛入門的朋友一些混淆,但是這兩個關鍵字使用的規則卻很簡單,先來看看如何使用 new 關鍵字。new(T) 宣告會直接拿到儲存位置,並且配置 Zero Value (初始化),也就是數字型態為 0,字串型態就是 ""。底下是範例程式

package main

import "fmt"

func main() {
  foo := new(int)
  fmt.Println(foo)
  fmt.Println(*foo)
  fmt.Printf("%#v", foo)
}

執行後可以看到底下結果

$ go run main.go 
0xc00001a110
0
(*int)(0xc00001a110)

上面的做法比較少人用,比較多人用在 struct 上面,由於 new 的特性,直接可以用在 struct 做初始化,底下是範例程式

package main

import (
  "bytes"
  "fmt"
  "sync"
)

type SyncedBuffer struct {
  lock   sync.Mutex
  buffer bytes.Buffer
  foo    int
  bar    string
}

func main() {
  p := new(SyncedBuffer)
  fmt.Println("foo:", p.foo)
  fmt.Println("bar:", p.bar)
  fmt.Printf("%#v\n", p)
}

上面可以看到透過 new 快速的達到初始化,但是有個不方便的地方就是,如果開發者要塞入特定的初始化值,透過 new 是沒辦法做到的,所以大多數的寫法會改成如下,範例連結

package main

import (
  "bytes"
  "fmt"
  "sync"
)

type SyncedBuffer struct {
  lock   sync.Mutex
  buffer bytes.Buffer
  foo    int
  bar    string
}

func main() {
  p := &SyncedBuffer{
    foo: 100,
    bar: "foobar",
  }
  fmt.Println("foo:", p.foo)
  fmt.Println("bar:", p.bar)
  fmt.Printf("%#v\n", p)
}

或者是大部分會寫一個新的 Func 做初始化設定,範例程式如下

package main

import (
  "bytes"
  "fmt"
  "sync"
)

type SyncedBuffer struct {
  lock   sync.Mutex
  buffer bytes.Buffer
  foo    int
  bar    string
}

func NewSynced(foo int, bar string) *SyncedBuffer {
  return &SyncedBuffer{
    foo: foo,
    bar: bar,
  }
}

func main() {
  p := NewSynced(100, "foobar")
  fmt.Println("foo:", p.foo)
  fmt.Println("bar:", p.bar)
  fmt.Printf("%#v\n", p)
}

但是 new 如果使用在 slice, mapchannel 身上的話,其初始的 Value 會是 nil,請看底下範例

package main

import (
  "fmt"
)

func main() {
  p := new(map[string]string)
  test := *p
  test["foo"] = "bar"
  fmt.Println(test)
}

底下結果看到 panic

$ go run main.go 
panic: assignment to entry in nil map

goroutine 1 [running]:
main.main()
        /app/main.go:10 +0x4f
exit status 2

初始化 map 拿到的會是 nil,故通常在宣告 slice, mapchannel 則會使用 Go 提供的另一個宣告方式 make

使用 make 關鍵字

makenew 不同的地方在於,new 回傳指標,而 make 不是,make 通常只用於在宣告三個地方,分別是 slice, mapchannel,如果真的想要拿到指標,建議還是用 new 方式。底下拿 map 當作範例

package main

import "fmt"

func main() {
  var p *map[string]string
  // new
  p = new(map[string]string)
  *p = map[string]string{
    "bar": "foo",
  }
  people := *p
  people["foo"] = "bar"

  fmt.Println(people)
  fmt.Println(p)

  // make
  foobar := make(map[string]string)
  foobar["foo"] = "bar"
  foobar["bar"] = "foo"
  fmt.Println(foobar)
}

上面例子可以看到 p 宣告為 map 指標,new 初始化 map 後則需要獨立寫成 map[string]string{},才可以正常運作,如果是透過 make 方式就可以快速宣告完成。通常是這樣,我自己在開發,幾乎很少用到 new,反到是在宣告 slice, mapchannel 時一定會使用到 make。記住,用 make 回傳的不會是指標,真的要拿到指標,請使用 new 的方式,但是程式碼就會變得比較複雜些。

心得

總結底下 makenew 的區別

  • make 能夠分配並且初始化所需要的記憶體空間跟結構,而 new 只能回傳指標位置
  • make 只能用在三種類型 slice, mapchannel
  • make 可以初始化上述三種格式的長度跟容量以便提供效率跟減少開銷

內嵌

在物件導向程式中,通常會用繼承來共享上層元件的程式碼。然而,go語言沒有繼承的特性,但我們能用組合的方式來共享程式碼。不僅如此,go語言還提供一種優於組合的語法特性,稱作內嵌。

組合(composition)

先來談談我所知道的組合,大部分的文章會講到組合是聚合(aggregation)的一種,而它們都是源自於UML的產物,實際上UML定義的定義很模糊也很難理解。因此,我要講的是它們最基本的一面,也就是 Is-AHas-A 關係:

  • Is-A: 繼承關係,表示一個物件也是另一個物件。
  • Has-A: 組合關係,表示一個物件擁有另一個物件。

很多文章和書都建議我們要多用組合少用繼承,這是因為繼承會對物件造成巨大的依賴關係。我們用一個範例來說明組合:

package main

import (
	"fmt"
)

// 定義一個英雄結構,包含了正常人結構
type Hero struct {
	Person   *Person
	HeroName string
	HerkRank int
}

// 定義一個正常人結構
type Person struct {
	Name string
}

func main() {
	var tony = &Hero{&Person{"Tony Stark"}, "Iron Man", 1}
	fmt.Printf("Hero=%+v\n", *tony)
	fmt.Printf("Person=%+v\n", *(tony.Person))
}

執行結果:

Hero={Person:0xc0000841e0 HeroName:Iron Man HerkRank:1}
Person={Name:Tony Stark}

上面範例中,我們看到了所謂的組合就是結構再包結構的概念,透過這樣的方式共享結構資料或方法。

內嵌(Embedding)

再來談談go語言的內嵌特性,這個特性並沒有寫在A Tour of Go,而是在Effective Go裡頭。

Go語言的內嵌其實就是組合的概念,只是它更加簡潔及強大。內嵌允許我們在結構內組合其他結構時,不需要定義欄位名稱,並且能直接透過該結構叫用欄位或方法。我們將上面的範例改成使用內嵌,如下:

package main

import (
	"fmt"
)

// 定義一個英雄結構
type Hero struct {
	*Person  // 不需要欄位名稱
	HeroName string
	HerkRank int
}

// 定義一個正常人結構
type Person struct {
	Name string
}

func main() {
	var tony = &Hero{
		&Person{"Tony Stark"},
		"Iron Man",
		1}

	fmt.Printf("%s\n", tony.Name) // 直接叫用內部結構資料
	// 等於 fmt.Printf("%s\n", tony.Person.Name)
}

實際上,內嵌的結構欄位還是會有名稱,就是和結構本身的名稱同名。

另外,上面範例是用匿名初始化,也可以使用具名初始化,差別在於初始化參數的數量和順序是可以被調整的:

var tony = &Hero{
		Person:   &Person{"Tony Stark"},
		HeroName: "Iron Man",
		HeroRank: 1}

內嵌與方法

上面看到的範例都是內嵌結構資料,現在我們來試試看內嵌結構方法,修改同一個範例如下:

package main

import (
	"fmt"
)

// 定義一個英雄結構
type Hero struct {
	*Person
	HeroName string
	HeroRank int
}

// 英雄都會飛
func (*Hero) Fly() {
	fmt.Println("I can fly.")
}

// 定義一個正常人結構
type Person struct {
	Name string
}

// 正常人會走路
func (p *Person) Walk() {
	fmt.Println("I can walk.")
	fmt.Println(p.Name)
}

func main() {
	var tony = &Hero{
		Person:   &Person{"Tony Stark"},
		HeroName: "Iron Man",
		HeroRank: 1}

	tony.Walk() // 等於 tony.Person.Walk()
	tony.Fly()
}

內嵌結構欄位同名

當有多個內嵌結構時,就有可能發生欄位同名的問題。我們稍微修改一下範例,超級英雄也會想養一隻寵物,這很合理的。因此,我們就加入一個寵物結構:

// 定義一個英雄結構
type Hero struct {
	*Person
	*Pet
	HeroName string
	HeroRank int
}

// 定義一個正常人結構
type Person struct {
	Name string
}

// 定義一個寵物結構
type Pet struct {
	Name string
}

func main() {
	var tony = &Hero{
		Person:   &Person{"Tony Stark"},
		Pet:   &Pet{"Pepper"},
		HeroName: "Iron Man",
		HeroRank: 1}

	fmt.Printf("%s\n", tony.Name)
}

由於 Person 和 Parner 都有 Name 這個欄位,直接叫用 tony.Name 就會產生衝突,編譯器會顯示錯誤訊息:

./main.go:40:25: ambiguous selector tony.Name

內嵌其他型別

事實上,可以被內嵌的型別不只有結構,也可以是基本型別,範例如下:

type Data struct {
	int
	string
	float32
	bool
}

func main() {
	var data = &Data{1, "Iron Man", 1.2, true}
	fmt.Println(*data)
	fmt.Printf("%+v \n", *data)
}

interface的使用要滿足2個條件才有意義:

  1. 實現了interface的幾個struct是相似關系(比如docker和kvm都是虛擬機)、平級的,並且輸入輸出參數完全一致。(這點是interface的本質,能實現interface的肯定是滿足這個條件)
  2. 在業務邏輯上,調用實現interface的struct是不確定的,是通過某種方式傳遞進來,而不是順序的業務邏輯,比如structA、structB、structC如果是有順序的則是錯誤的,下面這樣是錯誤的:
func main() {
    var i interfaceX
    i = &structA{...}
    i.Add()
    i = &structB{...}
    i.Add()
    i = &structC{...}
    i.Add()
}

這樣邏輯是正確的:

var i interfaceX
switch opt {
case "A":
    i = &structA{}
case "B":
    i = &structB{}
case "C":
    i = &structC{}
}
i.Add()
i.Del()

就是說調用者對於實現interface的struct是根據某個參數(通過API傳遞過來,或者配置文件傳遞過來,或者etcd傳遞過來)來選擇某個struct,這種邏輯才適用interface。而如果程序邏輯是被調用者依次執行,則不適用interface。

總結適用interface的調用者業務邏輯(偽代碼):

type I interface {
    ...
}

var i I
switch opt {    //opt通過某種方式傳遞進來,而不是寫死
case "A":
    i = &structA{...}
case "B":
    i = &structB{...}
case "C":
    i = &structC{...}
default:
    errors.New("not support")

interface使用起來有無數種變形方式,但無論是那種,都要符合上面說的平行選一的業務邏輯。

go interface使用場景

什麼是go 接口呢?學習過C++,一定知道C++ 的多態實現,而Golang 中 多態特性主要是通過接口來體現的。接口是由兩部分組成:一個方法集合,以及一個類型。首先我們將關注點集中到方法集合上。

  • interface{} 接口不是任何類型:它是一個 interface{} 類型

interface 類型可以定義一組方法,但是這些不需要實現。 interface 不能包含任何變量。 到某個自定義類型(比如結構體 Phone)要使用的時候,在根據具體情況把這些方法寫出來(實現)。

type  接口名 interface {
	method1(參數列表) 返回值列表
	method2(參數列表) 返回值列表
	...
}

type animal interface {
    Speak() string
}

定義一個animal 接口,供不同類型的animal 調用,不同的animal可以進行再次的實現

package main

import "fmt"

type Animal interface {
	Speak() string
}

type Dog struct {
}

func (d Dog) Speak() string {
	return "Dog :汪汪汪,在看就吃了你"
}

type Cat struct {
}

func (c Cat) Speak() string {
	return "Cat :喵星人, 不想理你,走開!!!"
}

type Fish struct {
}

func (f Fish) Speak() string {
	return "Fish :雖在水裡,但選擇逆流而上,絕不隨波逐流"
}

type Bird struct {
}

func (b Bird) Speak() string {
	return "Bird: 當我像鳥飛往你的山, 打破原有的束縛,創造新的機遇"
}

func main() {
	animals := []Animal{Dog{}, Cat{}, Fish{}, Bird{}}
	for _, animal := range animals {
		fmt.Println(animal.Speak())
	}
}

Difference between []*Users and *[]Users in Golang?

package main

import (
	"fmt"
)

type Users struct {
	ID   int
	Name string
}

var (
	userList []Users
)

func main() {
	//Make the slice of Users
	userList = []Users{Users{ID: 43215, Name: "Billy"}}

	//Then pass the slice as a reference to some function
	myFunc(&userList)

	fmt.Println(userList) // Outputs: [{1337 Bobby}]
}

//Now the function gets a pointer *[]Users that when changed, will affect the global variable "userList"
func myFunc(input *[]Users) {
	*input = []Users{Users{ID: 1337, Name: "Bobby"}}
}

package main

import (
	"fmt"
)

type Users struct {
	ID   int
	Name string
}

var (
	user1 Users
	user2 Users
)

func main() {
	//Make a couple Users:
	user1 = Users{ID: 43215, Name: "Billy"}
	user2 = Users{ID: 84632, Name: "Bobby"}

	//Then make a list of pointers to those Users:
	var userList []*Users = []*Users{&user1, &user2}

	//Now you can change an individual Users in that list.
	//This changes the variable user2:
	*userList[1] = Users{ID: 1337, Name: "Larry"}

	fmt.Println(user1) // Outputs: {43215 Billy}
	fmt.Println(user2) // Outputs: {1337 Larry}
}

在 Go 看 control flow 的輔助函式

出處: https://medium.com/@fcamel/%E5%9C%A8-go-%E7%9C%8B-control-flow-%E7%9A%84%E8%BC%94%E5%8A%A9%E5%87%BD%E5%BC%8F-7dfc07e88b86

看 C/C++ code 的時候,我習慣輸出 __FILE__, __LINE__, __FUNCTION__ 幫助看 control flow。透過 C 的巨集,很容易寫出高效率的輔助函式。

我在 Go 寫了類似的函式 Trace()。作法是取 runtime info,效率比 C/C++ 版本差,內容如下:

func Trace(format string, a ...interface{}) { 
  function, file, line, _ := runtime.Caller(1) 
  info := fmt.Sprintf("DEBUG> %s:%d %s:", path.Base(file), line,
                       runtime.FuncForPC(function).Name()) 
  msg := fmt.Sprintf(format, a...) 
  fmt.Println(info, msg)
}
package main

import (
	"github.com/fcamel/golang-practice/utils"
)

type myType struct {
}

func (t myType) hello() {
	utils.Trace("")
}

func foo() {
	utils.Trace("begin")
	defer utils.Trace("end")
	bar()
}

func bar() {
	utils.Trace("Hello %d", 101)
	var t myType
	t.hello()
}

func main() {
	foo()
}

執行結果:

$ go run cmd/trace/main.go
DEBUG> main.go:15 main.foo: begin
DEBUG> main.go:21 main.bar: Hello 101
DEBUG> main.go:11 main.myType.hello:
DEBUG> main.go:18 main.foo: end

美中不足的是,從 method 呼叫 Trace() 的時候,無法自動補上 object 的 address。要自己手動寫,像是這樣:

func (t myType) hello() {
  utils.Trace("%p", &t)
}

有多個 objects 呼叫一樣函式的時候,會不太方便。或許多研究一下 runtime 的功能,有機會作到?等受不了的時候,再研究看看。

find . -name '*.go' -exec sed -i '/import/a\"github.com/fcamel/golang-practice/utils"' {} \;

find . -name '*.go' -exec gofmt -l -w {} \;

go mod tidy

Golang學習筆記

出處: https://hackmd.io/Ku4_3XGMSAuRcFGxy8qTlA?both

tags: RD1

:::spoiler 目錄 [TOC] :::

Golang特點

為什麼 Golang 適合做為網頁後端程式的語言呢?

由於 Golang 有以下的優點:

  • Golang 易學易用:Golang 基本上是強化版的 C 語言,都以核心語法短小精要著稱
  • Golang 是靜態型別語言:很多程式的錯誤在編譯期就會挑出來,相對易於除錯
  • Golang 編譯速度很快:帶動整個開發的流程更快速
  • Golang 支援垃圾回收:網頁程式較接近應用程式,而非系統程式,垃圾回收在這個情境下不算缺點;此外,使用垃圾回收可簡化程式碼
  • Golang 內建共時性的語法:goroutine 比起傳統的執行緒 (thread) 來說輕量得多,在高負載時所需開銷更少
  • Golang 是跨平臺的:只要程式中不碰到 C 函式庫,在 Windows (或 Mac) 寫好的 Golang 網頁程式,可以不經修改就直接發布在 GNU/Linux 伺服器上
  • Golang 的專案不需額外的設定檔:在專案中,只要放 Golang 程式碼和一些 assets 即可運作,所需的工具皆內建在 Golang 主程式中,省去學習專案設罝的功夫
  • Golang 沒有死硬的程式架構:用 Golang 寫網頁程式思維上接近微框架 (micro-framework),只要少數樣板程式碼就可以寫出網頁程式,也不限定可用的第三方函式庫

但 Golang 並非完美無缺,以下是要考量的點:

  • Golang 並非完整的物件導向 (object-oriented) 語言,頂多是基於物件的 (object-based) 語言
  • Golang 的語言特性相對少:這是 Golang 時常被攻擊的點,這只能靠自己調整寫程式的習慣
  • 在一些情境下,Golang 程式碼相對笨拙冗餘,像是排序 (sorting)

開始一個專案

  1. 安裝好 go 以及設定 $GOPATH 環境
  2. VSCode設置
  3. 目錄結構
--src 放置專案的原始碼檔案
--pkg 放置編譯後生成的包 / 庫檔案
--bin 放置編譯後生成的可執行檔案
  1. mod
go mod init 初始化
go mod tidy 整理模組
  1. 測試囉(Gin、Mysql)
go get github.com/gin-gonic/gin
go get github.com/go-sql-driver/mysql

main.go

package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})

	r.Run(":8000")
}

mysql.go

package main

import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql"  //只引用該套件的init函數
)

func main() {
	db, err := sql.Open("mysql", "root:root@tcp(mysql)/test?charset=utf8")
	defer db.Close()

	//插入資料,使用預處理避免發生injection
	stmt, err := db.Prepare("INSERT userinfo SET username=?,department=?,created=?")
	checkErr(err)

	_, err = stmt.Exec("astaxie", "研發部門", "2012-12-09")
	checkErr(err)
}

func checkErr(err error) {
	if err != nil {
		panic(err)
	}
}

兩者都為 package main 代表他們本質上是一隻程式 只是分為不同檔案 不同的package之間需分為不同資料夾,並互相引用: "module_name/floder_name" ex.import router(別名) "main/routes" Go 實作 Restful API

Go的資料型態

Go的資料類別一共分為四大類:

  1. 基本型別(Basic type): 數字、字串、布林值
  2. 聚合型別(Aggregate type): 陣列、結構
  3. 參照型別(Reference type): 指標、slice、map、function、channel
  4. 介面型別(Interface type)

變數宣告

var a          // 不定型別的變數
var a int      // 宣告成 int
var msg string // 宣告成 string
var a int = 10 // 初始化同時宣告
var a = 10     // 會自動幫你判定為整數型別
var a, b int   // a 跟 b 都是 intvar a, b = 0
var a int , b string
var a, b, c int = 1, 2, 3
var a, b, c = 1, 2, 3
var(
    a bool = false // 記得要不同行,不然會錯
    b int
    c = "hello"
)

// 在函數中,「:=」 簡潔賦值語句在明確類型的地方,可以替代 var 定義。
//「:=」 結構不能使用在函數外,函數外的每個語法都必須以關鍵字開始。
// := 只能用在宣告
var msg = "Hello World"

等於

msg := "Hello World" //自動判定型態

a := 0
a, b, c := 0, true, "tacolin" // 這樣就可以不同型別寫在同一行

_, b := 34, 35 // _(下劃線)是個特殊的變數名,任何賦予它的值都會被丟棄。

布林值

在Go中 bool 與 int 不能直接轉換,true,false 不直接等於 1 與 0

整數

型態描述
int88-bit signed integer
int1616-bit signed integer
int3232-bit signed integer
int6464-bit signed integer
uint88-bit unsigned integer
uint1616-bit unsigned integer
uint3232-bit unsigned integer
uint6464-bit unsigned integer
intBoth in and uint contain same size, either 32 or 64 bit.
uintBoth in and uint contain same size, either 32 or 64 bit.
rune等價 unit32 ,表示一個Unicode字符
byte等價 uint8 ,表示一個ASCII字符
uintptrIt is an unsigned integer type. Its width is not defined, but its can hold all the bits of a pointer value.

浮點數

型態描述
float3232-bit IEEE 754 floating-point number
float6464-bit IEEE 754 floating-point number

複數

型態描述
complex64Complex numbers which contain float32 as a real and imaginary component.
complex128Complex numbers which contain float64 as a real and imaginary component.

字串

var mystr01 string = "\\r\\n"
等於
var mystr02 string = `\r\n`

輸出:\r\n

`` 表示一個多行的字串

陣列

陣列

// 第一種方法
var arr = [3]int{1,2,3} //%T = [3]int

// 第二種方法
arr := [3]int{1,2,3}

// 第三種方法
arr := [...]int{1,2,3} // 可以省略長度而採用`...`的方式,Go 會自動根據元素個數來計算長度

//注意類型為字串時
var arr = [3]string{
    "first",
    "second",
    "third", //最後這裡要有逗號
}

切片

為一個左閉右開的結構

//宣告一個空的切片
var arr []int    //默認值為nil

運用make( []Type, size, cap )指定類型、長度、容量, 建立一個容量為10,目前長度為3的切片:

make([]int, 3, 10)    //make( []Type, size, cap )
  • 輸出
arr[0:2] 
//-->[1 2] 結尾索引不算在內
  • append
myarr := []int{1}
// 追加一個元素
myarr = append(myarr, 2)
// 追加多個元素
myarr = append(myarr, 3, 4)
// 追加一個切片, ... 表示解包,不能省略
myarr = append(myarr, []int{7, 8}...)
// 在開頭插入元素0
myarr = append([]int{0}, myarr[0:]...) //[0:]為開頭的話可省略
// 在中間插入一個切片(兩個元素)
myarr = append(myarr[:5], append([]int{5, 6}, myarr[5:]...)...)

fmt.Println(myarr) //--> [0 1 2 3 4 7 8]
  • copy
slice1 := []int{1,2,3}
slice2 := make([]int, 2)
copy(slice2, slice1) 
fmt.Println(slice1, slice2)
// 由於slice2容量只有2所以只有slice1[0:2]被複製過去
// 輸出結果: [1 2 3] [1 2]

字典

  • 宣告
// 第一種方法
var scores map[string]int = map[string]int{"english": 80, "chinese": 85}

// 第二種方法
scores := map[string]int{"english": 80, "chinese": 85}

// 第三種方法
scores := make(map[string]int)
scores["english"] = 80
scores["chinese"] = 85
  • 新增 / 讀取 / 更新 / 刪除
scores["math"] = 95
scores["math"] = 100 //若已存在,直接更新
delete( scores, "math" )
fmt.Println(scores["math"]) //不存在則返回value-type的0值
//-->100
  • 判斷是否存在字典裡
elements := map[string]string{
    "H": "Hydrogen",
    "He": "Helium",
    "Li": "Lithium",
    "Be": "Beryllium"
}

value, isExist := elements["H"];
// value = Hydrogen, isExist = true

value, isExist := elements["A"];
// value = "", isExist = false
  • 巢狀字典
elements := map[string]map[string]string{
    "H": map[string]string{
        "name":"Hydrogen",
        "state":"gas",
    },
    "He": map[string]string{
        "name":"Helium",
        "state":"gas",
    },
    "Li": map[string]string{
        "name":"Lithium",
        "state":"solid",
    },
    "Be": map[string]string{
        "name":"Beryllium",
        "state":"solid",
    },
    "B":  map[string]string{
        "name":"Boron",
        "state":"solid",
    },
    "C":  map[string]string{
        "name":"Carbon",
        "state":"solid",
    },
    "N":  map[string]string{
        "name":"Nitrogen",
        "state":"gas",
    },
    "O":  map[string]string{
        "name":"Oxygen",
        "state":"gas",
    },
    "F":  map[string]string{
        "name":"Fluorine",
        "state":"gas",
    },
    "Ne":  map[string]string{
        "name":"Neon",
        "state":"gas",
    },
}

if el, ok := elements["Li"]; ok {
    fmt.Println(el["name"], el["state"])
}

Struct

自定義型別,struct裡可以放struct型別的物件 參考資料

type person struct {
    name string
    height int
}

json & struct

  • 宣告 Struct fields must start with upper case letter (exported) for the JSON package to see their value.
type Message struct {
	Sender  string `json:"sender"`
	RoomId  string `json:"roomId"`
	Content string `json:"content"`
	Time    string `json:"time"`
}
  • 放入資料產生[]byte 格式的 json 資料
jsonMessage, _ := json.Marshal(&Message{Sender: c.id, RoomId: c.roomId, Content: string(message), Time: time})
  • 解回struct物件
var msg Message
json.Unmarshal(message, &msg)

指標

跟C語言一樣,Go語言也有指標。

func zero( x *int ) {
    *x = 0
}

func main() {
    x := 5
    zero( &x )
    fmt.Println( x )
}

介面 interface

package main

import "fmt"
import "math"

type geometry interface {
    area() float64
    perimeter() float64
}

type square struct {
    width, height float64
}
type circle struct {
    radius float64
}

func (s square) area() float64 {
    return s.width * s.height
}
func (s square) perimeter() float64 {
    return 2*s.width + 2*s.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
}

func main() {
    s := square{width: 3, height: 4}
    c := circle{radius: 5}

    measure(s)
    measure(c)
}

控制語句

迴圈

for

Go只有一種迴圈關鍵字,就是for

func main() {
    sum := 0
    for i := 0; i < 10; i++ {
        sum += i
    }
    fmt.Println(sum)
}

跟 C 或者 Java 中一樣,可以讓前置、後置語句為空。

func main() {
    sum := 1
    for ; sum < 1000; {
        sum += sum
    }
    fmt.Println(sum)
}

基於此可以省略分號:C 的 while 在 Go 中叫做 「for」。

func main() {
    sum := 1
    for sum < 1000 {
        sum += sum
    }
    fmt.Println(sum)
}

如果省略了迴圈條件,迴圈就不會結束,因此可以用更簡潔地形式表達無窮迴圈。

func main() {
    for {
        fmt.Println("Hello World")
    }
}

陣列尋訪

可以這樣尋訪

var x [4]float64{ 23, 45, 33, 21 }
var total float64 = 0
for i := 0; i < 4; i++ {
    total += x[i]
}
fmt.Println( total / float64(4))

使用len獲取陣列元素數量

var x [4]float64{ 23, 45, 33, 21 }
var total float64 = 0
for i := 0; i < len(x); i++ {
    total += x[i]
}
fmt.Println( total / float64(len(x)))

更精簡一點

var x [4]float64{ 23, 45, 33, 21 }
var total float64 = 0

for i, value := range x {
    total += value
}
fmt.Println( total / float64(len(x)))

for迴圈前面的第一個變數意義為陣列索引(index),而後面變數代表該索引值所代表的陣列值。以上寫法會出錯,由於Go不允許沒有使用的變數出現在程式碼中,迴圈的i變數我們使用佔位符(_)替代。

func main() {
    var x [4]float64{ 23, 45, 33, 21 }
    var total float64 = 0

    for _, value := range x {
        total += value
    }
    fmt.Println( total / float64(len(x)))
}

分支 break、continute、goto

break

可以利用break提前退出循環。

func main() {
    for i := 0; i < 10; i++ {
        if i > 5 {
            break       
        }
        fmt.Println(i)
    }
}

如果有多重迴圈,可以指定要跳出哪一個迴圈,但需要指定標籤。

func main() {
outer: // 標籤在此
    for j := 0; j < 5; j++ {
        for i := 0; i < 10; i++ {
            if i > 6 {
                break outer 
            }
            fmt.Println(i)
        }
    }
}

continute

continue忽略之後的程式碼,直接執行下一次迭代。

func main() {
    for i := 1; i <= 10; i++ {
        if i < 6 {
            continue
        }
        fmt.Println(i)
    }
}

同樣的如果有多重迴圈,也可以指定標籤。

func main() {
outer: // 標籤在此
    for i := 1; i < 10; i++ {
        for j := 1; j < 10; j++ {
            if i == j {
                continue outer
            }
            fmt.Println( "i: ", i, " j: ", j );
        }
    }
}

goto

Go 語言跟 C 語言一樣也有「 goto 」,但是不建議使用,會讓程式的結構變得很糟糕。

func main() {
    i := 0
HERE:
    fmt.Print(i)
    i++
    if i < 10 {
        goto HERE
    }
}

defer、panic、recover

此範例文章取自openhome.cc

就許多現代語言而言,例外處理機制是基本特性之一,然而,例外處理是好是壞,一直以來存在著各種不同的意見,在 Go 語言中,沒有例外處理機制,取而代之的,是運用 defer、panic、recover 來滿足類似的處理需求。

defer

在 Go 語言中,可以使用 defer 指定某個函式延遲執行,那麼延遲到哪個時機?簡單來說,在函式 return語句之後準備返回呼叫的函式之前,例如:

  • 延遲效果
func myfunc() {
    fmt.Println("B")
}

func main() {
    defer myfunc()
    fmt.Println("A")
}

輸出

A
B
  • 可在返回之前修改返回值
package main

import "fmt"

func Triple(n int) (r int) {
	defer func() {
		r += n // 修改返回值
	}()

	return n + n // <=> r = n + n; return
}

func main() {
	fmt.Println(Triple(5))
}

輸出

15
  • 變數的快照
func main() {
    name := "go"
    defer fmt.Println(name) // 變數name的值被記住了,所以會輸出go

    name = "python"
    fmt.Println(name)      // 輸出: python
}

輸出

python
go
  • 應用
  1. 反序調用 如果有多個函式被 defer,那麼在函式 return 前,會依 defer 的相反順序執行,也就是 LIFO,例如:
package main

import "fmt"

func deferredFunc1() {
    fmt.Println("deferredFunc1")
}

func deferredFunc2() {
    fmt.Println("deferredFunc2")
}

func main() {
    defer deferredFunc1()
    defer deferredFunc2()
    fmt.Println("Hello, 世界")
}

// 輸出結果:
Hello, 世界
deferredFunc2
deferredFunc1
  1. defer 與 return
func f() {
    r := getResource()  //0,獲取資源
    ......
    if ... {
        r.release()  //1,釋放資源
        return
    }
    ......
    if ... {
        r.release()  //2,釋放資源
        return
    }
    ......
    r.release()  //3,釋放資源
    return


}

使用 defer 後,不論在哪 return 都會執行 defer 後方的函數,如此便不用在每個return前寫上r.release()

func f() {
    r := getResource()  //0,獲取資源

    defer r.release()  //1,釋放資源
    ......
    if ... {
        ...
        return
    }
    ......
    if ... {
        ...
        return
    }
    ......
    return
}

以下是清除資源的範例:

package main

import (
    "fmt"
    "os"
)

func main() {
    f, err := os.Open("/tmp/dat")
    if err != nil {
        fmt.Println(err)
        return;
    }

    defer func() { // 延遲執行,而且函式 return 後一定會執行
        if f != nil {
            f.Close()
        }
    }()

    b1 := make([]byte, 5)
    n1, err := f.Read(b1)
    if err != nil {
        fmt.Printf("%d bytes: %s\n", n1, string(b1))
        // 處理讀取的內容....
    }
}

panic 恐慌中斷

如果在函式中執行 panic,那麼函式的流程就會中斷,若 A 函式呼叫了 B 函式,而 B 函式中呼叫了 panic,那麼 B 函式會從呼叫了 panic 的地方中斷,而 A 函式也會從呼叫了 B 函式的地方中斷,若有更深層的呼叫鏈,panic 的效應也會一路往回傳播。

package main

import (
    "fmt"
    "os"
)

func check(err error) {
    if err != nil {
        panic(err)
    }
}

func main() {
    f, err := os.Open("/tmp/dat")
    check(err)

    defer func() {
        if f != nil {
            f.Close()
        }
    }()

    b1 := make([]byte, 5)
    n1, err := f.Read(b1)
    check(err)

    fmt.Printf("%d bytes: %s\n", n1, string(b1))
}

如果在開啟檔案時,就發生了錯誤,假設這是在一個很深的呼叫層次中發生,若你直接想撰寫程式,將 os.Open 的 error 逐層傳回,那會是一件很麻煩的事,此時直接發出 panic,就可以達到想要的目的。

recover

如果發生了 panic,而你必須做一些處理,可以使用 recover,這個函式必須在被 defer 的函式中執行才有效果,若在被 defer 的函式外執行,recover 一定是傳回 nil。

如果有設置 defer 函式,在發生了 panic 的情況下,被 defer 的函式一定會被執行,若當中執行了 recover,那麼 panic 就會被捕捉並作為 recover 的傳回值,那麼 panic 就不會一路往回傳播,除非你又呼叫了 panic。

因此,雖然 Go 語言中沒有例外處理機制,也可使用 defer、panic 與 recover 來進行類似的錯誤處理。例如,將上頭的範例,再修改為:

package main

import (
    "fmt"
    "os"
)

func check(err error) {
    if err != nil {
        panic(err)
    }
}

func main() {
    f, err := os.Open("/tmp/dat")
    check(err)

    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err) // 這已經是頂層的 UI 介面了,想以自己的方式呈現錯誤
        }

        if f != nil {
            if err := f.Close(); err != nil {
                panic(err) // 示範再拋出 panic
            }
        }
    }()

    b1 := make([]byte, 5)
    n1, err := f.Read(b1)
    check(err)

    fmt.Printf("%d bytes: %s\n", n1, string(b1))
}

條件判斷

if、else、else if

if 條件一 {    
  分支一
} else if 條件二 {
  分支二
} else if 條件 ... {
  分支 ...
} else {
  分支 else
}

// { 必須與if..在同一行

&& : 且 || : 或

在 if 裡允許先運行一個表達式,取得變數後再來做判斷:

func main() {
    if age := 20;age > 18 {
        fmt.Println("已成年")
    }
}

switch

與一般的switch宣告方法一樣,條件不能重複

  • 一個case多個條件
import "fmt"

func main() {
    month := 2

    switch month {
    case 3, 4, 5:
        fmt.Println("春天")
    case 6, 7, 8:
        fmt.Println("夏天")
    case 9, 10, 11:
        fmt.Println("秋天")
    case 12, 1, 2:
        fmt.Println("冬天")
    default:
        fmt.Println("輸入有誤...")
    }
}
  • switch 後可接函數
import "fmt"

// 判斷一個同學是否有掛科記錄的函數
// 返回值是布爾類型
func getResult(args ...int) bool {
    for _, i := range args {
        if i < 60 {
            return false
        }
    }
    return true
}

func main() {
    chinese := 80
    english := 50
    math := 100

    switch getResult(chinese, english, math) {
    // case 後也必須 是布爾類型
    case true:
        fmt.Println("該同學所有成績都合格")
    case false:
        fmt.Println("該同學有掛科記錄")
    }
}
  • switch 後面不接東西時就相當於if-else
  • 使用fallthrough 可以往下穿透一層,執行下一個case語句且不用判斷條件,但其必須為該case的最後一個語句,否則會錯誤

Go 函式

  1. 一般用法
func add( x int, y int ) int {
	return x + y
}

func main() {
	fmt.Println( add( 42, 13 ) )
}

當兩個或多個連續的函數命名參數是同一類型,則除了最後一個類型之外,其他都可以省略。 所以如果參數的型態都一樣的話,可以精簡為:

func add( x, y int ) int {  
    return x + y  
}  
 
func main() {  
    fmt.Println( add( 42, 13 ) )  
}  
  1. 多數值返回

函數可以返回任意數量的返回值,這個函數返回了兩個字串。

func swap(x, y string) (string, string) {  
    return y, x  
}  
 
func main() {  
    a, b := swap("hello", "world")  
    fmt.Println(a, b)  
}
// 輸出結果 world hello
  1. 命名返回值

在 Go 中,函數可以返回多個「結果參數」,而不僅僅是一個值。它們可以像變數那樣命名和使用。 如果命名了返回值參數,一個沒有參數的 return 語句,會將當前的值作為返回值返回。以這個程式碼為例,sum int 表示宣告整數 sum ,將參數 17 放入 sum 中,x, y int 宣告整數 x,y 在下面使用,由於 return 沒有設定返回值,這邊程式就將 x,y 都回傳了,所以結果會出現 7 10。

func split(sum int) (x, y int) {  
    x = sum * 4 / 9  
    y = sum - x  
    return  
}  
 
func main() {  
    fmt.Println(split(17))  
}

Goroutine

要使用Goroutine只要在呼叫的函數前面加一個go關鍵字即可

package main
import "fmt"  
 
func f(n int) {  
    for i := 0; i < 10; i++ {  
        fmt.Println(n, ":", i)  
    }  
}  
 
func main() {  
    go f(0)  
} 

執行後會發現什麼東西都沒有印出,因為 goroutine 是平行處理的, 所以在還沒開始印 n 之前 main 這個主要的函式已經結束了。 使用內建的 time 函式讓 main 函式等 goroutine 先跑完。

package main  
 
import (  
    "fmt"  
    "time"  
)  
 
func f(n int) {  
    for i := 0; i < 10; i++ {  
        fmt.Println(n, ":", i)  
    }  
}  
 
func main() {  
    go f(0)  
    time.Sleep(time.Second * 1) // 暫停一秒鐘  
}  

龜兔賽跑的範例

此龜兔賽跑範例文章引用自openhome.cc/Go/Goroutine

先來看個沒有啟用 Goroutine,卻要寫個龜兔賽跑遊戲的例子,你可能是這麼寫的:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func random(min, max int) int {
    rand.Seed(time.Now().Unix())
    return rand.Intn(max-min) + min
}

func main() {
    flags := [...]bool{true, false}
    totalStep := 10
    tortoiseStep := 0
    hareStep := 0
    fmt.Println("龜兔賽跑開始...")
    for tortoiseStep < totalStep && hareStep < totalStep {
        tortoiseStep++
        fmt.Printf("烏龜跑了 %d 步...\n", tortoiseStep)
        isHareSleep := flags[random(1, 10)%2]
        if isHareSleep {
            fmt.Println("兔子睡著了zzzz")
        } else {
            hareStep += 2
            fmt.Printf("兔子跑了 %d 步...\n", hareStep)
        }
    }
}

由於程式只有一個流程,所以只能將烏龜與兔子的行為混雜在這個流程中撰寫,而且為什麼每次都先遞增烏龜再遞增兔子步數呢?這樣對兔子很不公平啊!如果可以撰寫程式再啟動兩個流程,一個是烏龜流程,一個兔子流程,程式邏輯會比較清楚。

你可以將烏龜的流程與兔子的流程分別寫在一個函式中,並用 go 啟動執行:

package main

import (
	"fmt"
	"math/rand"
    "time"
)

func random( min, max int ) int {
	rand.Seed( time.Now().Unix() )
	return rand.Intn( max - min ) + min
}

func tortoise( totalStep int ) {
	for step := 1; step <= totalStep; step++ {
		fmt.Printf( "烏龜跑了 %d 步...\n", step )
	}
}

func hare(totalStep int) {
    flags := [...]bool{true, false}
    step := 0
    for step < totalStep {
        isHareSleep := flags[random(1, 10)%2]
        if isHareSleep {
            fmt.Println("兔子睡著了zzzz")
        } else {
            step += 2
            fmt.Printf("兔子跑了 %d 步...\n", step)
        }
    }
}

func main() {
    totalStep := 10

    go tortoise(totalStep)
    go hare(totalStep)

    time.Sleep(5 * time.Second) // 給予時間等待 Goroutine 完成
}

使用sync.WaitGroup等待烏龜與兔子跑完

有沒有辦法知道 Goroutine 執行結束呢?實際上沒有任何方法可以得知,除非你主動設計一種機制,可以在 Goroutine 結束時執行通知,使用 Channel 是一種方式,這在之後的文件再說明,這邊先說明另一種方式,也就是使用 sync.WaitGroup。

sync.WaitGroup 可以用來等待一組 Goroutine 的完成,主流程中建立 sync.WaitGroup,並透過 Add 告知要等待的 Goroutine 數量,並使用 Wait 等待 Goroutine 結束,而每個 Goroutine 結束前,必須執行 sync.WaitGroup 的 Done 方法。

因此,我們可以使用 sync.WaitGroup 來改寫以上的範例:

package main

import (
	"fmt"
	"math/rand"
	"time"
	"sync"
)

func random( min, max int ) int {
	rand.Seed( time.Now().Unix() )
	return rand.Intn( max - min ) + min
}

func tortoise( totalStep int, wg *sync.WaitGroup ) {
	defer wg.Done()

	for step := 1; step <= totalStep; step++ {
		fmt.Printf( "烏龜跑了 %d 步...\n", step )
	}
}

func hare(totalStep int, wg *sync.WaitGroup ) {
	defer wg.Done()

    flags := [...]bool{true, false}
    step := 0
    for step < totalStep {
        isHareSleep := flags[random(1, 10)%2]
        if isHareSleep {
            fmt.Println("兔子睡著了zzzz")
        } else {
            step += 2
            fmt.Printf("兔子跑了 %d 步...\n", step)
        }
    }
}

func main() {
	wg := new( sync.WaitGroup )
	wg.Add( 2 )

    totalStep := 10

    go tortoise( totalStep, wg )
    go hare( totalStep, wg )

    time.Sleep(5 * time.Second) // 給予時間等待 Goroutine 完成
}

Channel

通過 Channel 可以讓 goroutine 之間通信

ch_name := make(chan <TYPE>{,NUM})    //類型與大小

資料流向

  • 向Channel傳入:Ch <- DATA
  • 從Channel讀取:DATA := <- Ch
func main() {  
 
    messages := make(chan string)  
 
    go func() { messages <- "ping" }()  
 
    msg := <- messages  
    fmt.Println( msg )  
}  
  • 建立一個 channel(message) 用以傳輸字串
  • 用 go 來 call goroutine 執行函式,傳 "ping" 到 messages 這個 channel 裡面
  • 接著以 msg 負責接收 messages 的傳輸資料後印出

透過這個方法就可以簡單的讓 Goroutine 可以溝通

select

有一個類似 Switch 的流程控制「Select」,它只能應用於 Channel

package main

import "time"
import "fmt"

func main() {

    c1 := make(chan string)
    c2 := make(chan string)

    go func() {
        time.Sleep(time.Second * 1)
        c1 <- "one"
    }()
    go func() {
        time.Sleep(time.Second * 2)
        c2 <- "two"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }
}

Go Coding Style

  1. 強制編碼風格 Go語言為了讓團隊開發能夠更加的簡單,他統一了程式碼的風格,如果沒有遵照他的規範寫的話,你再如何編譯都不會成功。 以下為錯誤的程式碼風格

    package main
    import "fmt"
    func main()
    {
        i:= 1
        fmt.Println("Hello World", i)
    }
    

    如果你左右括弧的寫法是像上面那樣,你將會看到下列的錯誤訊息

    syntax error: unexpected semicolon or newline before {non-declaration 
    statement outside function body syntax error: unexpected }
    

    以下為正確的寫法

    package main
    import "fmt"
    func main() {
        i:= 1
        fmt.Println("Hello World", i)
    }
    

    為了保持程式碼的乾淨,你宣告了一個變數,但是卻沒有使用,Go 語言連編譯都不會讓你編譯。舉例來說,變數 i 並沒有被使用。

    package main
    
    import "fmt"
    
    func main() {
        i := 1
        fmt.Println("Hello World i")
    }
    

    你會出現下列錯誤訊息

    # command-line-arguments
    ./test.go:6:2: i declared but not used
    
  2. 非強制性編譯風格建議 以下程式碼可以正常的編譯,但是很醜不好閱讀。

    package main
    import "fmt"
    func main() { i:= 1
    fmt.Println("Hello World", i)}
    

    我們可以利用go fmt指令幫忙整理程式碼編譯格式。 用法 go fmt <filename>.go # 整理某個檔案 go fmt *.go # 整理目錄下所有go檔案 go fmt # 同上

    如果程式碼不需要調整他不會出現任何訊息,成功會出現你使用的程式檔名。 格式化工具幫你做到了下列事情:

    • 調整每一條語句的位置
    • 重新擺放括弧的位置
    • 以 tab 幫你縮排
    • 添加空格

套件

Go套件的一些規則

Go之所以會那麼簡潔,是因為它有一些預設的行為:

  • 大寫字母開頭的變數是可匯出的,也就是其它套件可以讀取的,是公有變數;小寫字母開頭的就是不可匯出的,是私有變數。
  • 大寫字母開頭的函式也是一樣,相當於 class 中的帶 public 關鍵詞的公有函式;小寫字母開頭的就是有 private 關鍵詞的私有函式。

gRPC

GO gRPC

官方 - Quick start 範例 - Hello ,gRPC How we use gRPC to build a client/server system in Go 比起 JSON 更方便、更快速、更簡短的 Protobuf 格式 API 文件就是你的伺服器,REST 的另一個選擇:gRPC

gRPC and HTTP APIs

比較 gRPC 服務與 HTTP API 同時提供HTTP接口 gRPC-Web:envoy 如果兩邊都想要 - gRPC Gateway

參考資料

[1] Go (Golang) 適合初學者做為網頁後端程式嗎? [2] Golang — GOROOT、GOPATH、Go-Modules-三者的關係介紹 [3] GeeksforGeeks: Data Types in Go [4] 初學Golang30天 [5] Go 語言設計與實現 - make 和 new [6] Opencc Go [7] 使用 Golang 打造 Web 應用程式 [8] 五分鐘理解golang的init函數 [9] Go標準庫:Go template用法詳解 [10] How to use local go modules with golang with examples [11] Go併發編程模型:主動停止goroutine [12] Go gin框架入門教程 [13] Golang 套件初始化流程 [14] Go語言變數的生命週期 [15] 使用golang的mysql無法插入emoji表情的問題 [16] Go語言高級編程(Advanced Go Programming) [17] Golang中range的使用方法及注意事項 [18] Go語言101

範例補充資料

gorilla/websocket - example:chat Build a Realtime Chat Server With Go and WebSockets Go Websocket 長連線

pytago

onlone

  • https://pytago.dev/

Running pre-built container

  • https://github.com/nottheswimmer/pytago
docker run -p 8080:8080 -e PORT=8080 -it nottheswimmer/pytago

學習筆記之第1章 Go基礎入門

第1章 Go基礎入門

1.1 安裝Go

https://golang.google.cn/dl/

1.2 第一個Go程序

package main

import "fmt"

func main() {
	fmt.Println("Hello World~")
}
  1. 包聲明

包管理單位。

package xxx
  • 目錄下同級文件屬於同一個包
  • 包名與目錄名可以不同
  • 有且僅有一個main包(入口包)
  1. 包導入

調用其他包的變量或方法。

import "package_name"
import (
	"os"
	"fmt"
)
別名
import (
	alias1 "os"
	alias2 "fmt"
)
import (
	_ "os" //只初始化包(調用包中init函數),不使用包中變量或函數。
	alias2 "fmt"
)
  1. main函數

入口函數,只能聲明在main包中,有且僅有一個。

func 函數名(參數列表) (返回值列表) {
	函數體
}

1.3 Go基礎語法與使用

1.3.1 基礎語法

  1. Go語言標記

Go程序由關鍵字、標識符、常量、字符串、符號等多種標記組成。

fmt . Println ( "Hi" )
  1. 行分隔符

一般一行一個語句,多個語句用;隔開。

  1. 注釋

//單行注釋 /* 多行注釋 多行注釋 */

  1. 標識符

標識符通常用來對變量、類型等命名。[a-zA-Z0-9_]組成,不能以數字開始,不能是Go語言關鍵字。

  1. 字符串連接
package main

import "fmt"

func main() {
	fmt.Println("hello" + " world")
}
  1. 關鍵字
continueforimportreturnvar
constfallthroughifrangetype
chanelsegotopackageswith
casedefergomapstruct
breakdefaultfuncinterfaceselect
  • 常量相關預定義標識符:true、false、ioto、nil
  • 類型相關預定義標識符:int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64、uintptr、float32、float64、complex128、complex64、bool、byte、rune、string、error
  • 函數相關預定義標識符:make、len、cap、new、append、copy、close、delete、complex、real、imag、panic、recover
  1. Go語言空格

var name string name = “y” + “x”

1.3.2 變量

變量(variable)是一段或多段用來存儲數據的內存,有明確類型。

var name type
var c, d *int

默認零值或空值,int為0,float為0.0,bool為false,string為"",指針為nil。 建議駝峰命名法totalPrice或下劃線命名法total_price。

var (
	age int
	name string
	balance float32
)

名字 := 表達式 簡短模式(short variable declaration)限制:

  • 只用於定義變量,同時顯示初始化
  • 表達式自動推導數據類型
  • 用於函數內部,即不能聲明全局變量
package main

import (
	"fmt"
)

func main() {
	//var 變量名 [類型] = 變量值
	var language1 string = "Go"
	fmt.Printf("language1=%s\n", language1)
	var language2 = "Go"
	fmt.Printf("language2=%s\n", language2)
	
	//變量名 := 變量值
	language3 := "Go"
	fmt.Printf("language3=%s\n", language3)
	
	/*
	var (
		變量名1 [變量類型1] = 變量值1
		變量名2 [變量類型2] = 變量值2
	)
	*/
	var (
		age1 int = 18
		name1 string = "yx"
		balance1 = 999.9
	)
	fmt.Printf("age1=%d, name1=%s, balance1=%f\n", age1, name1, balance1)
	
	//var 變量名1, 變量名2 = 變量值1, 變量值2
	var age2, name2, balance2 = 18, "yx", 999.9
	fmt.Printf("age2=%d, name2=%s, balance2=%f\n", age2, name2, balance2)
	
	//變量名1, 變量名2 := 變量值1, 變量值2
	age3, name3, balance3 := 18, "yx", 999.9
	fmt.Printf("age3=%d, name3=%s, balance3=%f\n", age3, name3, balance3)
	
	//變量交換值
	d, c := "D", "C"
	fmt.Printf("d=%s, c=%s\n", d, c)
	c, d = d, c
	fmt.Printf("d=%s, c=%s\n", d, c)
}

局部變量,函數體內聲明的變量,參數和返回值變量都是局部變量。

package main

import "fmt"

func main() {
	var local1, local2, local3 int

	local1 = 8
	local2 = 10
	local3 = local1 + local2 
	
	fmt.Printf("local1=%d, local2=%d, local3=%d\n", local1, local2, local3)
}

全局變量,函數體外聲明的變量,可以在整個包甚至外部包(被導出)中使用,也可在任何函數中使用。

package main

import "fmt"

var global int

func main() {
	var local1, local2 int

	local1 = 8
	local2 = 10
	global = local1 + local2 
	
	fmt.Printf("local1=%d, local2=%d, global=%d\n", local1, local2, global)
}
package main

import "fmt"

var global int = 8

func main() {
	var global int = 99
	fmt.Printf("global=%d\n", global)
}

1.3.3 常量

const聲明,編譯時創建(聲明在函數內部也是),存儲不會改變的數據,只能是布爾型、數字型(整數、浮點和復數)和字符串型。

package main

import (
	"fmt"
)

//const 常量名 [類型] = 常量表達式
const PI float32 = 3.1415926

//itoa用於生成一組以相似規則初始化的常量。
type Direction int
const (
	North Direction = iota
	East
	South
	West
)

/*
常量間算術、邏輯、比較運算都是常量。
常量進行類型轉換,返回常量結果。
len(),cap(),real(),imag(),complex()和unsafe.Sizeof()等函數調用返回常量結果。
*/
const IPv4Len = 4
func paraseIPv4(s string) ([4]byte, error) {
	var p [IPv4Len]byte	
	return p, nil
}

func main() {
	const (
		e = 2.7182818
		pi = 3.1415926
	)

	fmt.Printf("PI=%v\n", PI)
	fmt.Printf("e=%v, pi=%v\n", e, pi)
	
	fmt.Printf("West=%v\n", West)

	if ip, err := paraseIPv4("192.168.1.1"); err != nil {
		fmt.Printf("ip=%v\n", ip)
	}
}

6種未明確類型的常量類型:

  • 無類型的布爾型(true和false)
  • 無類型的整數(0)
  • 無類型的字符(\u0000)
  • 無類型的浮點數(0.0)
  • 無類型的復數(0i)
  • 無類型的字符串("")

延遲明確常量的具體類型,可以直接用於更多的表達式而不需要顯示的類型轉換。

package main

import (
	"math"
	"fmt"
)

func main() {
	var a float32 = math.Pi
	var b float64 = math.Pi
	var c complex128 = math.Pi
	fmt.Printf("a=%v, b=%v, c=%v\n", a, b, c)
	
	const Pi64 float64 = math.Pi
	a = float32(Pi64)
	b = Pi64
	c = complex128(Pi64)
	fmt.Printf("a=%v, b=%v, c=%v\n", a, b, c)
}

1.3.4 運算符

運算符是用來在程序運行時執行數學運算或邏輯運算的符號。

package main

import (
	"fmt"
)

func main() {
	var a, b, c = 3, 6, 9
	d := a + b * c
	fmt.Printf("d=%v\n", d)
}

優先級是指,同一表達式中多個運算符,先執行哪一個。

優先級分類運算符結合性
1逗號運算符,從左到右
2賦值運算符=、+=、-=、*=、/=、%=、>=、<<=、&=、^=、|=從右到左
3邏輯或||從左到右
4邏輯與&&從左到右
5按位或|從左到右
6按位異或^從左到右
7按位與&從左到右
8等不等==、!=從左到右
9關系運算符<、<=、>、>=從左到右
10位移運算符<<、>>從左到右
11加減法+、-從左到右
12乘除法取餘*(乘號)、/、%從左到右
13單目運算符!、*(指針)、&(取址)、++、–、+(正號)、-(負號)從右到左
14後綴運算符()、[]從左到右

1.3.5 流程控制語句

if-else
package main

import (
	"fmt"
)

func if_else_return(b int) int {
	if b > 10 {
		return 1
	} else if b == 10 {
		return 2
	} else {
		return 3
	}
}

func main() {
	fmt.Println(if_else_return(10))
}

for Go不支持while和do while。

package main

import (
	"fmt"
)

func main() {
	product := 1
	for i := 1; i < 5; i++ {
		product *= i
	}
	fmt.Println(product)

	i := 0
	for {
		if i > 50 {
			break
		}
		i++
	}
	fmt.Println(i)

	j := 2
	for ; j > 0; j-- {
		fmt.Println(j)
	}

JumpLoop:
	for i := 0; i < 5; i++ {
		for j := 0; j < 5; j++ {
			if i > 2 {
				break JumpLoop
			}
			fmt.Println(i)
			if j == 2 {
				continue
			}
		}
	}
}

for-range 可以遍歷數組、切片、字符串、map和channel。

for key, val := range 復合變量值 {
	//val對應索引值的復制值,只讀。
	//修改val值,不會影響原有集合中的值。
}
for position, runeChar := range str {
	//
}
package main

import (
	"fmt"
)

func main() {
	//遍歷數組、切片
	for key, value := range []int{0, 1, -1, -2} {
		fmt.Printf("key:%d value:%d\n", key, value)
	}

	//遍歷字符串
	var str = "hi 加油"
	for key, value := range str {
		fmt.Printf("key:%d value:0x%x\n", key, value)
	}

	//遍歷map
	m1 := map[string]int{
		"go":  100,
		"web": 100,
	}
	//輸出無序
	for key, value := range m1 {
		fmt.Printf(key, value)
	}

	//遍歷通道
	c := make(chan int)
	go func() {
		c <- 7
		c <- 8
		c <- 9
		close(c)
	}()
	for v := range c {
		fmt.Println(v)
	}

	//_匿名變量,佔位符,不參與空間分配,也不佔用變量名字。
	m2 := map[string]int{
		"go":  100,
		"web": 100,
	}
	for _, v := range m2 {
		fmt.Println(v)
	}
	for key, _ := range []int{0, 1, -1, -2} {
		fmt.Printf("key:%d\n", key)
	}
}

swith-case 表達式不必為常量,甚至整數,不需通過break跳出,各case中類型一致。

package main

import (
	"fmt"
)

func main() {
	var a = "love"
	switch a {
	default:
		fmt.Println("none")
	case "love":
		fmt.Println("love")
	case "programming":
		fmt.Println("programming")
	}
	switch a {
	default:
		fmt.Println("none")
	case "love", "programming":
		fmt.Println("find")
	}

	var r int = 6
	switch {
	case r > 1 && r < 10:
		fmt.Println(r)
	}
}
goto
package main

import (
	"fmt"
)

func main() {
	var isBreak bool
	for x := 0; x < 20; x++ {
		for y := 0; y < 20; y++ {
			if y == 2 {
				isBreak = true
				break
			}
		}
		if isBreak {
			break
		}
	}
	fmt.Println("over")
}
package main

import (
	"fmt"
)

func main() {
	for x := 0; x < 20; x++ {
		for y := 0; y < 20; y++ {
			if y == 2 {
				goto breakTag
			}
		}
	}
breakTag:
	fmt.Println("over")
}

goto在多錯誤處理中優勢

func main() {
	err := getUserInfo()
	if err != nil {
		fmt.Println(err)
		exitProcess()
	}

	err = getEmail()
	if err != nil {
		fmt.Println(err)
		exitProcess()
	}
	
	fmt.Println("over")
}
func main() {
	err := getUserInfo()
	if err != nil {
		goto doExit
	}

	err = getEmail()
	if err != nil {
		goto doExit
	}
	
	fmt.Println("over")
	return

doExit:
	fmt.Println(err)
	exitProcess()
}

1.4 Go數據類型

類型說明
布爾型true或false
數字類型uint8、uint16、uint32、uint64、int8、int16、int32、int64 、float32(IEEE-754)、float64(IEEE-754)、complex64、complex128、byte(uint8)、rune(int32)、uint(32或64)、int(32或64)、uintptr(存放指針)
字符串類型一串固定長度的字符連接起來的字符序列,utf-8編碼
復合類型數組、切片、map、結構體

1.4.1 布爾型

只有兩個相同類型的值才能比較:

  • 值的類型是接口(interface),兩者必須都實現了相同的接口。
  • 一個是常量,另一個不是常量,類型必須和常量類型相同。
  • 類型不同,必須轉換為相同類型,才能比較。

&&優先級高於||,有短路現象。

package main

import (
	"fmt"
)

func bool2int(b bool) int {
	if b {
		return 1
	} else {
		return 0
	}
}

func int2bool(i int) bool { return i != 0 }

func main() {
	fmt.Println(bool2int(true))
	fmt.Println(int2bool(0))
}

1.4.2 數字類型

位運算採用補碼。int、uint和uintptr,長度由操作系統類型決定。

1.4.3 字符串類型

由一串固定長度的字符連接起來的字符序列,utf-8編碼。值類型,字節的定長數組。

//聲明和初始化
str := "string"

字符串字面量用"或`創建

  • "創建可解析的字符串,支持轉義,不能引用多行
  • `創建原生的字符串字面量,不支持轉義,可多行,不能包含反引號字符
str1 := "\"hello\"\nI love you"
str2 := `"hello"
I love you
`
//字符串連接
str := "I love" + " Go Web"
str += " programming"
package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	str := "我喜歡Go Web"
	fmt.Println(len(str))
	fmt.Println(utf8.RuneCountInString(str))
	fmt.Println(str[9])
	fmt.Println(string(str[9]))
	fmt.Println(str[:3])
	fmt.Println(string(str[:3]))
	fmt.Println(str[3:])
	fmt.Println([]rune(str))
}
package main

import (
	"fmt"
)

func main() {
	str := "我喜歡Go Web"
	chars := []rune(str)
	for ind, char := range chars {
		fmt.Printf("%d: %s\n", ind, string(char))
	}
	for ind, char := range str {
		fmt.Printf("%d: %s\n", ind, string(char))
	}
	for ind, char := range str {
		fmt.Printf("%d: %U %c\n", ind, char, char)
	}
}
var buffer bytes.Buffer
for {
	if piece, ok := getNextString(); ok {
		buffer.WriteString(piece)
	} else {
		break
	}
}
fmt.Println(buffer.String())

不能通過str[i]方式修改字符串中的字符。 只能將字符串內容復制到可寫變量([]byte或[]rune),然後修改。轉換類型過程中會自動復制數據。

str := "hi 世界"
by := []byte(str)
by[2] = ','
fmt.Printf("%s\n", str)
fmt.Printf("%s\n", by)
fmt.Printf("%s\n", string(by))
str := "hi 世界"
by := []rune(str)
by[3] = '中'
by[4] = '國'
fmt.Println(str)
fmt.Println(by)
fmt.Println(string(by))

1.4.4 指針類型

指針類型指存儲內存地址的變量類型。

var b int = 66
var p * int = &b
package main

import (
	"fmt"
)

func main() {
	var score int = 100
	var name string = "barry"
	fmt.Printf("%p %p\n", &score, &name)
}
package main

import (
	"fmt"
)

func main() {
	var address string = "hangzhou, China"
	ptr := &address
	
	fmt.Printf("address type: %T\n", address)
	fmt.Printf("address value: %v\n", address)
	fmt.Printf("address address: %p\n", &address)
	
	fmt.Printf("ptr type: %T\n", ptr)
	fmt.Printf("ptr value: %v\n", ptr)
	fmt.Printf("ptr address: %p\n", &ptr)
	fmt.Printf("point value of ptr : %v\n", *ptr)
}
package main

import (
	"fmt"
)

func exchange1(c, d int) {
	t := c
	c = d
	d = t
}

func exchange2(c, d int) {
	c, d = d, c
}

func exchange3(c, d *int) {
	t := *c
	*c = *d
	*d = t
}

func exchange4(c, d *int) {
	d, c = c, d
}

func exchange5(c, d *int) {
	*d, *c = *c, *d
}

func main() {
	x, y := 6, 8
	x, y = y, x
	fmt.Println(x, y)

	x, y = 6, 8
	exchange1(x, y)
	fmt.Println(x, y)
	
	x, y = 6, 8
	exchange2(x, y)
	fmt.Println(x, y)
	
	x, y = 6, 8
	exchange3(&x, &y)
	fmt.Println(x, y)
	
	x, y = 6, 8
	exchange4(&x, &y)
	fmt.Println(x, y)
	
	x, y = 6, 8
	exchange5(&x, &y)
	fmt.Println(x, y)
}

1.4.5 復合類型

  1. 數組類型

數組是具有相同類型(整數、字符串、自定義類型等)的一組長度固定的數據項的序列。

var array [10]int
var numbers = [5]float32{100.0, 8.0, 9.4, 6.8, 30.1}
var numbers = [...]float32{100.0, 8.0, 9.4, 6.8, 30.1}
package main

import (
	"fmt"
)

func main() {
	var arr [6]int
	var i, j int
	for i = 0; i < 6; i++ {
		arr[i] = i + 66
	}
	for j = 0; j < 6; j++ {
		fmt.Printf("arr[%d] = %d\n", j, arr[j])
	}
}
  1. 結構體類型

結構體是由0或多個任意類型的數據構成的數據集合。

type 類型名 struct {
	字段1 類型1
	結構體成員2 類型2
}
type Pointer struct {
	A float32
	B float32
}

type Color struct {
	Red, Green, Blue byte
}

variable_name := struct_variable_type {value1, value2, ...}
variable_name := struct_variable_type {key2: value2, key1: value1, ...}
package main

import "fmt"

type Book struct {
	title string
	author string
	subject string
	press string
}

func main() {
	fmt.Println(Book{author: "yx", title: "學習 Go Web"})
	
	var bookGo Books
	bookGo.title = "學習 Go Web"
	bookGo.author = "yx"
	bookGo.subject = "Go"
	bookGo.press = "電力工業出版社"
	fmt.Printf("bookGo.title: %s\n", bookGo.title)
	fmt.Printf("bookGo.author: %s\n", bookGo.author)
	fmt.Printf("bookGo.subject: %s\n", bookGo.subject)
	fmt.Printf("bookGo.press: %s\n", bookGo.press)
	printBook(bookGo)
	printBook(&bookGo)
}

func printBook(book Books) {
	fmt.Printf("book.title: %s\n", book.title)
	fmt.Printf("book.author: %s\n", book.author)
	fmt.Printf("book.subject: %s\n", book.subject)
	fmt.Printf("book.press: %s\n", book.press)
}

func printBook2(book *Books) {
	fmt.Printf("book.title: %s\n", book.title)
	fmt.Printf("book.author: %s\n", book.author)
	fmt.Printf("book.subject: %s\n", book.subject)
	fmt.Printf("book.press: %s\n", book.press)
}
  1. 切片類型

slice是對數組或切片連續片段的引用。 切片內部結構包含內存地址pointer、大小len和容量cap。

//不含結束位置
slice[開始位置:結束位置]
var sliceBuilder [20]int
for i := 0; i < 20; i++ {
	sliceBuilder[i] = i + 1
}
fmt.Println(sliceBuilder[5:15])
fmt.Println(sliceBuilder[15:])
fmt.Println(sliceBuilder[:2])
b := []int{6, 7, 8}
fmt.Println(b[:])
fmt.Println(b[0:0])
var sliceStr []string
var sliceNum []int
var emptySliceNum = []int{}
fmtp.Println(sliceStr, sliceNum, emptySliceNum)
fmtp.Println(len(sliceStr), len(sliceNum), (emptySliceNum))
fmtp.Println(sliceStr == nil, sliceNum == nil, emptySliceNum == nil)
slice1 := make([]int, 6)
slice2 := make([]int, 6, 10)
fmtp.Println(slice1, slice2)
fmtp.Println(len(slice1), len(slice2))
fmtp.Println(cap(slice1), cap(slice2))
  1. map類型

關聯數組,字典,元素對(pair)的無序集合,引用類型。

var name map[key_type]value_type
var literalMap map[string]string
var assignedMap map[string]string
literalMap = map[string]string{"first": "go", "second": "web"}
createdMap := make(map[string]float32)
assignedMap = literalMap	//引用
createdMap["k1"] = 99
createdMap["k2"] = 199
assignedMap["second"] = "program"

fmt.Println(literalMap["first"])
fmt.Println(literalMap["second"])
fmt.Println(literalMap["third"])
fmt.Println(createdMap["k2"])
createdMap := new(map[string]float32)
//錯誤
//聲明瞭一個未初始化的變量並取了它的地址
//map到達容量上限,自動增1
make(map[key_type]value_type, cap)
map := make(map[string]float32, 100)

achievement := map[string]float32{
	"zhang": 99.5, "xiao": 88,
	"wange": 96, "ma": 100,
}
map1 := make(map[int][]int)
map2 := make(map[int]*[]int)

1.5 函數

1.5.1 聲明函數

func function_name([parameter list]) [return_types] {
	//bunction_body
}
package main

import "fmt"

func main() {
	array := []int{6, 8, 10}
	var ret int
	ret = min(array)
	fmt.Println("最小值是: %d\n", ret)
}

func min(arr []int) (m int) {
	m = arr[0]
	for _, v := range arr {
		if v < m {
			m = v
		}
	}
	return
}
package main

import "fmt"

func compute(x, y int) (int, int) {
	return x+y, x*y
}
func main() {
	a, b := compute(6, 8)
	fmt.Println(a, b)
}
package main

import "fmt"

func change(a, b int) (x, y int) {
	x = a + 100
	y = b + 100
	return
	//return x, y
	//return y, x
}
func main() {
	a := 1
	b := 2
	c, d := compute(a, b)
	fmt.Println(c,d)
}

1.5.2 函數參數

  1. 參數使用
  • 形參:定義函數時,用於接收外部傳入的數據。
  • 實參:調用函數時,傳給形參的實際的數據。
  1. 可變參數
func myFunc(arg ...string) {
	for _, v := range arg {
		fmt.Printf("the string is: %s\n", v)
	}
}
  1. 參數傳遞
  • 值傳遞
package main

import "fmt"

func exchange(a, b int) {
	var tmp int
	tmp = a
	a = b
	b = tmp
}
func main() {
	a := 1
	b := 2
	fmt.Printf("交換前a=%d\n", a)
	fmt.Printf("交換前b=%d\n", b)
	exchange(a, b)
	fmt.Printf("交換後a=%d\n", a)
	fmt.Printf("交換後b=%d\n", b)
}
  • 引用傳遞
package main

import "fmt"

func exchange(a, b *int) {
	var tmp int
	tmp = *a
	*a = *b
	*b = tmp
}
func main() {
	a := 1
	b := 2
	fmt.Printf("交換前a=%d\n", a)
	fmt.Printf("交換前b=%d\n", b)
	exchange(&a, &b)
	fmt.Printf("交換後a=%d\n", a)
	fmt.Printf("交換後b=%d\n", b)
}

1.5.3 匿名函數

匿名函數(閉包),一類無須定義標識符(函數名)的函數或子程序。

  1. 定義
func (參數列表) (返回值列表) {
	//函數體
}
package main

import "fmt"

func main() {
	x, y := 6, 8
	defer func(a int) {
		fmt.Println("defer x, y = ", a, y) //y為閉包引用
	}(x)
	x += 10
	y += 100
	fmt.Println(x, y)
}
/*
輸出
16 108
defer x,y = 6 108
*/
  1. 調用
  • 定義時調用
package main

import "fmt"

func main() {
	//定義匿名函數並賦值給f變量
	f := func(data int) {
		fmt.Println("closure", data)
	}
	f(6)

	//直接聲明並調用
	func(data int) {
		fmt.Println("closure, directly", data)
	}(8)
}
  • 回調函數(call then back)
package main

import "fmt"

func visitPrint(list []int, f func(int)) {
	for _, value := range list {
		f(value)
	}
}

func main() {
	sli := []int{1, 6, 8}
	visitPrint(sli, func(value int) {
		fmt.Println(value)
	})
}

1.5.4 defer延遲語句

defer用於函數結束(return或panic)前最後執行的動作,便於及時的釋放資源(數據庫連接、文件句柄、鎖等)。

defer語句執行邏輯:

  1. 函數執行到defer時,將defer後的語句壓入專門存儲defer語句的棧中,然後繼續執行函數下一個語句。
  2. 函數執行完畢,從defer棧頂依次取出語句執行(先進後出,後進先出)。
  3. defer語句放在defer棧時,相關值會復制入棧中。
package main

import "fmt"

func main() {
	deferCall()
}

func deferCall() {
	defer func1()
	defer func2()
	defer func3()
}

func func1() {
	fmt.Println("A")
}

func func2() {
	fmt.Println("B")
}

func func3() {
	fmt.Println("C")
}

//輸出
//C
//B
//A
package main

import "fmt"

var name string = "go"

func myfunc() string {
	defer func() {
		name = "python"	//最後一個動作,修改全局變量name為"python"
	}()

	fmt.Printf("myfunc()函數裡的name: %s\n", name)//全局變量name("go")未修改
	return name	//倒數第二個動作,將全局變量name("go")賦值給myfunc函數返回值
}

func main() {
	myname := myfunc()
	fmt.Printf("main()函數裡的name: %s\n", name)
	fmt.Printf("main()函數裡的myname: %s\n", myname)
}

//輸出
//myfunc()函數裡的name: go
//main()函數裡的name: python
//main()函數裡的myname: go

defer常用應用場景:

  1. 關閉資源。 創建資源(數據庫連接、文件句柄、鎖等)語句下一行,defer語句註冊關閉資源,避免忘記。
  2. 和recover()函數一起使用。 程序宕機或panic時,recover()函數恢復執行,而不報錯。
func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()	//func()函數含recover,不可封裝成外部函數調用,必須defer func(){}()匿名函數調用
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}

1.6 Go面向對象編程

1.6.1 封裝

隱藏對象屬性和實現細節,僅公開訪問方式。 Go使用結構體封裝屬性。

type Triangle struct {
	Bottom float32
	Height float32
}

方法(Methods)是作用在接收者(receiver)(某種類型的變量)上的函數。

func (recv recv_type) methodName(parameter_list) (return_value_list) {...}
package main

import "fmt"

type Triangle struct {
	Bottom float32
	Height float32
}

func (t *Triangle) Area() float32 {
	return (t.Bottom * t.Height) / 2
}

func main() {
	t := Triangle(6, 8)
	fmt.Println(t.Area())
}

訪問權限指類屬性是公開還是私有的,Go通過首字母大小寫來控制可見性。 常量、變量、類型、接口、結構體、函數等若是大寫字母開頭,則能被其他包訪問或調用(public);非大寫開頭則只能包內使用(private)。

package person
type Student struct {
	name string
	score float32
	Age int
}

package pkg
import (
	person
	"fmt"
)
s := new(person.Student)
s.name = "yx" //錯誤
s.Age = 22
fmt.Println(s.Age)
package person

type Student struct {
	name string
	score float32
}

func (s *Student) GetName() string {
	return s.name
}

func (s *Student) SetName(newName string) {
	s.name = newName
}

package main

import (
	person
	"fmt"
)

func main() {
	s := new(person.Student)
	s.SetName("yx")
	s.Age = 22
	fmt.Println(s.GetName())
}

1.6.2 繼承

結構體中內嵌匿名類型的方法來實現繼承。

type Engine interface {
	Run()
	Stop()
}

type Bus struct {
	Engine
}

func (c *Bus) Working() {
	c.Run()
	c.Stop()
}

1.6.3 多態

多態指不同對象中同種行為的不同實現方法,通過接口實現。

package main

import (
	"fmt"
)

type Shape interface {
	Area() float32
}

type Square struct {
	sideLen float32
}

func (sq *Square) Area() float32 {
	return sq.sideLen * sq.sideLen
}

type Triangle struct {
	Bottom float32
	Height float32
}

func (t *Triangle) Area() float32 {
	return t.Bottom * t.Height
}

func main() {
	t := &Tri8angle{6, 8}
	s := &Square{}
	shapes := []Shape{t, s}
	for n, _ := range shapes {
		fmt.Println("圖形數據:", shapes[n])
		fmt.Println("面積:", shapes[n].Area())
	}
}

1.7 接口

1.7.1 接口定義

接口類型是對其他類型行為的概括與抽象,定義了零及以上個方法,但沒具體實現這些方法。 接口本質上是指針類型,可以實現多態。

//接口定義格式
type 接口名稱 interface {
	method1(參數列表) 返回值列表
	method2(參數列表) 返回值列表
	//...
	methodn(參數列表) 返回值列表
}

空接口(interface{}),無任何方法聲明,類似面向對象中的根類型,c中的void*,默認值nil。實現接口的類型支持相等運算,才能比較。

var var1, var2 interface{}
fmt.Println(var1 == nil, var1 == var2)
var1, var2 = 66, 88
fmt.Println(var1 == var2)
//比較map[string]interface{}
func CompareTwoMapInterface(data1 map[string]interface{}, data2 map[string]interface{}) bool {
	keySlice := make([]string, 0)
	dataSlice1 := make([]interface{}, 0)
	dataSlice2 := make([]interface{}, 0)
	for key, value := range data1 {
		keySlice = append(keySlice, key)
		dataSlice1 = append(dataSlice1, value)
	}
	for _, key := range keySlice {
		if data, ok := data2[key]; ok {
			dataSlice2 = append(dataSlice2, data)
		} else {
			return false
		}
	}
	dataStr1, _ := json.Marshal(dataSlice1)
	dataStr2, _ := json.Marshal(dataSlice2)

	return string(dataStr1) == string(dataStr2)
}

1.7.2 接口賦值

接口不支持直接實例化,但支持賦值操作。

  1. 實現接口的對象實例賦值給接口

要求該對象實例實現了接口的所有方法。

type Num int

func (x Num) Equal(i Num) bool {
	return x == i
}

func (x Num) LessThan(i Num) bool {
	return x < i
}

func (x Num) MoreThan(i Num) bool {
	return x > i
}

func (x *Num) Multiple(i Num) {
	*x = *x * i
}

func (x *Num) Divide(i Num) {
	*x = *x / i
}

type NumI interface {
	Equal(i Num) bool
	LessThan(i Num) bool
	MoreThan(i Num) bool
	Multiple(i Num)
	Divide(i Num)
}

//&Num實現NumI所有方法
//Num未實現NumI所有方法
var x Num = 8
var y NumI = &x

/*
Go語言會根據非指針成員方法,自動生成對應的指針成員方法
func (x Num) Equal(i Num) bool
func (x *Num) Equal(i Num) bool
*/
  1. 一個接口賦值給另一個接口

兩個接口擁有相同的方法列表(與順序無關),則等同,可相互賦值。

package oop1

type NumInterface1 interface {
	Equal(i int) bool
	LessThan(i int) bool
	BiggerThan(i int) bool
}

package oop2

type NumInterface2 interface {
	Equal(i int) bool
	BiggerThan(i int) bool
	LessThan(i int) bool
}

type Num int

//int不能改為Num
func (x Num) Equal(i int) bool {
	return int(x) == i
}

func (x Num) LessThan(i int) bool {
	return int(x) < i
}

func (x Num) BiggerThan(i int) bool {
	return int(x) > i
}

var f1 Num = 6
var f2 oop1.NumInterface1 = f1
var f3 oop2.NumInterface2 = f2

若接口A的方法列表是接口B的方法列表的子集,則接口B可以直接賦值給接口A。

type NumInterface1 interface {
	Equal(i int) bool
	LessThan(i int) bool
	BiggerThan(i int) bool
}

type NumInterface2 interface {
	Equal(i int) bool
	BiggerThan(i int) bool
	LessThan(i int) bool
	Sum(i int)
}

type Num int

func (x Num) Equal(i int) bool {
	return int(x) == i
}

func (x Num) LessThan(i int) bool {
	return int(x) < i
}

func (x Num) BiggerThan(i int) bool {
	return int(x) > i
}

func (x *Num) Sum(i int) {
	*x = *x + Num(i)
}

var f1 Num = 6
var f2 NumInterface2 = &f1
var f3 NumInterface1 = f2

1.7.3 接口查詢

程序運行時,詢問接口指向的對像是否時某個類型。

var filewriter Write = ...
if filew, ok := filewriter.(*File); ok {
	//...
}
slice := make([]int, 0)
slice = append(slice, 6, 7, 8)
var I interface{} = slice
if res, ok := I.([]int); ok {
	fmt.Println(res) //[6 7 8]
	fmt.Println(ok) //true
}
func Len(array interface{}) int {
	var length int
	
	switch b := array.(type) {
	case nil:
		length = 0
	case []int:
		length = len(b)
	case []string:
		length = len(b)
	case []float32:
		length = len(b)
	default:
		length = 0
	}
	return length
}

1.7.4 接口組合

接口間通過嵌套創造出新接口。

type Interface1 interface {
	Write(p []byte) (n int, err error)
}

type Interface2 interface {
	Close() error
}

type InterfaceCombine interface {
	Interface1
	Interface2
}

1.7.5 接口應用

  1. 類型推斷

類型推斷可將接口還原為原始類型,或用來判斷是否實現了某種更具體的接口類型。

package main

import "fmt"

func main() {
	var a interface{} = func(a int) string {
		rteurn fmt.Sprintf("d:%d", a)
	}
	
	switch b := a.(type) {
	case nil:
		fmt.Println("nil")
	case *int:
		fmt.Println(*b)
	case func(int) string:
		fmt.Println(b(66))
	case fmt.Stringer:
		fmt.Println(b)
	default:
		fmt.Println("unknown")
	}
}
  1. 實現多態功能
package main

import "fmt"

type Message interface {
	sending()
}

type User struct {
	name string
	phone string
}

func (u *User) sending() {
	fmt.Printf("Sending user phone to %s<%s>\n", u.name, u.phone)
}

type admin struct {
	name string
	phone string
}

func (a *admin) sending() {
	fmt.Printf("Sending admin phone to %s<%s>\n", a.name, a.phone)
}

func main() {
	bill := User{"Barry", "barry@gmail.com"}
	sendMessage(&bill)
	
	lisa := admin{"Barry", "barry@gmail.com"}
	sendMessage(&lisa)
}

func sendMessage(n Message) {
	n.sending()
}

1.8 反射

1.8.1 反射的定義

反射指,編譯時不知道變量的具體類型,運行時(Run time)可以訪問、檢測和修改狀態或行為的能力。

reflect包定義了接口和結構體,獲取類型信息。

  • reflect.Type接口提供類型信息
  • reflect.Value結構體提供值相關信息,可以獲取甚至改變類型的值
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value

1.8.2 反射的三大法則

  1. 接口類型變量轉換為反射類型對象
package main

import (
	"fmt"
	"reflect"
)

func main() {
	var x float64 = 3.4
	fmt.Println("type:", reflect.TypeOf(x))
	fmt.Println("value:", reflect.ValueOf(x))
	
	v := reflect.ValueOf(x)
	fmt.Println("type:", v.Type())
	fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
	fmt.Println("value:", v.Float())
}
//輸出
//type: float64
//value: 3.4
//kind is float64: true
//type: float64
//value: 3.4
  1. 反射類型對象轉換為接口類型變量
func (v Value) Interface() interface{}
y := v.Interface().(float64)
fmt.Println(y)
package main

import (
	"fmt"
	"reflect"
)

func main() {
	var name interface{} = "shirdon"
	fmt.Printf("原始接口變量類型為%T,值為%v\n", name, name)
	
	t := reflect.TypeOf(name)
	v := reflect.ValueOf(name)
	fmt.Printf("Type類型為%T,值為%v\n", t, t)
	fmt.Printf("Value類型為%T,值為%v\n", v, v)
	
	i := v.Interface()
	fmt.Printf("新對象interface{}類型為%T,值為%v\n", i, i)
}
//輸出
//原始接口變量類型為string,值為shirdon
//Type類型為*reflect.rtype,值為string
//Value類型為reflect.Value,值為shirdon
//新對象interface{}類型為string,值為shirdon
  1. 修改反射類型對象,其值必須是可寫的(settable)

reflect.TypeOf()和reflect.ValueOf()函數中若傳遞的不是指針,則只是變量復制,對該反射對象修改,不會影響原始變量。 反射對象可寫性要點:

  • 變量指針創建的反射對象
  • CanSet()可判斷
  • Elem()返回指針指向的數據
package main

import (
	"fmt"
	"reflect"
)

func main() {
	var name string = "shirdon"
	//var name int = 12
	
	v1 := reflect.ValueOf(&name)
	v2 := v1.Elem()
	fmt.Println("可寫性:", v1.CanSet())
	fmt.Println("可寫性:", v2.CanSet())
}

//輸出
//可寫性:false
//可寫性:true
func (v Value) SetBool(x bool)
func (v Value) SetBytes(x []byte)
func (v Value) SetFloat(x float64)
func (v Value) SetInt(x int64)
func (v Value) SetString(x string)
package main

import (
	"fmt"
	"reflect"
)

func main() {
	var name string = "shirdon"
	fmt.Println("name原始值:", name)
	
	v1 := reflect.ValueOf(&name)
	v2 := v1.Elem()
	
	v2.SetString("yx")
	fmt.Println("反射對象修改後,name值:", name)
}

//輸出
//name原始值: shirdon
//反射對象修改後,name值: yx

1.9 goroutine簡介

每一個並發執行的活動叫goroutine。

go func_name()
package main

import (
	"fmt"
	"time"
)

func hello() {
	fmt.Println("hello")
}

func main() {
	go hello()
	time.Sleep(1*time.Second)
	fmt.Println("end")
}

1.10 單元測試(go test)

testing庫,*_test.go文件。

//sum.go
package testexample

func Min(arr []int) (min int) {
	min = arr[0]
	for _, v := range arr {
		if v < min {
			min = v
		}
	}
	return
}

//sum_test.go
package testexample

import (
	"fmt"
	"testing"
)

func TestMin(t *testing.T) {
	array := []int{6, 8, 10}
	ret := Min(array)
	fmt.Println(ret)
}

//go test
//go test -v
//go test -v -run="Test"
參數作用
-v打印每個測試函數的名字和運行時間
-c生成測試可執行文件,但不執行,默認命名pkg.test
-i重新安裝運行測試依賴包,但不編譯和運行測試代碼
-o指定生成測試可執行文件的名稱

1.11 Go編譯與工具

1.11.1 編譯(go build)

//build
//----main.go
//----utils.go

//main.go
package main

import (
	"fmt"
)

func main() {
	printString()
	fmt.Println("go build")
}

//utils.go
package main

import "fmt"

func printString() {
	fmt.Println("test")
}

//cd build
//go build
//go build main.go utils.go
//go build -o file.exe main.go utils.go
//pkg
//----mainpkg.go
//----buildpkg.go

//mainpkg.go
package main

import (
	"fmt"
	"pkg"
)

func main() {
	pkg.CallFunc()
	fmt.Println("go build")
}

//buildpkg.go
package pkg

import "fmt"

func CallFunc() {
	fmt.Println("test")
}
//go build .../pkg
//compile.go
package main

import (
	"fmt"
)

func main() {
	fmt.Println("go build")
}
//CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build compile.go
  • CGO_ENABLED: 是否使用C語言的Go編譯器;
  • GOOS:目標操作系統
  • GOARCH:目標操作系統的架構
系統編譯參數架構
linux(>=Linux 2.6)386 / amd64 / arm
darwin(OS X(Snow Lepoard + Lion))386 / amd64
freebsd(>=FreeBSD 7)386 / amd64
windows(>=Windows 2000)386 / amd64
附加參數作用
-v編譯時顯示包名
-p n開啟並發編譯,默認值為CPU邏輯核數
-a強制重新構建
-n打印編譯時會用到的所有命令,但不真正執行
-x打印編譯時會用到的所有命令
-race開啟競態檢測

1.11.2 編譯後運行(go run)

編譯後直接運行,且無可執行文件。

//hello.go
package main

import (
	"fmt"
)

func main() {
	fmt.Println("go run")
}
//go run hello.go

1.11.3 編譯並安裝(go install)

類似go build,只是編譯中間文件放在$GOPATH/pkg目錄下,編譯結果放在$GOPATH/bin目錄下。

//install
//|----main.go
//|----pkg
//    |----installpkg.go

//main.go
package main

import (
	"fmt"
	"pkg"
)

func main() {
	pkg.CallFunc()
	fmt.Println("go build")
}

//installpkg.go
package pkg

import "fmt"

func CallFunc() {
	fmt.Println("test")
}
//go install

1.11.4 獲取代碼(go get)

動態遠程拉取或更新代碼包及其依賴包,自動完成編譯和安裝。需要安裝Git,SVN,HG等。

標記名稱標記描述
-d只下載,不安裝
-f使用-u時才有效,忽略對已下載代碼包導入路徑的檢查。適用於從別人處Fork代碼包
-fix下載代碼包後先修正,然後編譯和安裝
-insecure運行非安全scheme(如HTTP)下載代碼包。
-t同時下載測試源碼文件中的依賴代碼包
-u更新已有代碼包及其依賴包
go get -u github.com/shirdon1/TP-Link-HS110

Web開發基礎

第2章 Go Web開發基礎

2.1 helloWorldWeb

//helloWorldWeb.go
//go run helloWorldWeb.go
//127.0.0.1
package main
import (
	"fmt"
	"net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World")
}

func main() {
	server := &http.Server {
		Addr: "0.0.0.0:80",
	}
	http.HandleFunc("/", hello)
	server.ListenAndServe()
}

2.2 Web程序運行原理簡介

2.2.1 Web基本原理

  1. 運行原理 (1)用戶打開客戶端瀏覽器,輸入URL地址。 (2)客戶端瀏覽器通過HTTP協議向服務器端發送瀏覽請求。 (3)服務器端通過CGI程序接收請求,調用解釋引擎處理“動態內容”,訪問數據庫並處理數據,通過HTTP協議將得到的處理結果返回給客戶端瀏覽器。 (4)客戶端瀏覽器解釋並顯示HTML頁面。
  2. DNS(Domain Name System,域名系統) 將主機名和域名轉換為IP地址。 DNS解析過程: (1)用戶打開瀏覽器,輸入URL地址。瀏覽器從URL中抽取域名(主機名),傳給DNS應用程序的客戶端。 (2)DNS客戶端向DNS服務器端發送查詢報文,其中包含主機名。 (3)DNS服務器端向DNS客戶端發送回答報文,其中包含該主機名對應IP地址。 (4)瀏覽器收到DNS的IP地址後,向該IP地址定位的HTTP服務器端發起TCP連接。

2.2.2 HTTP簡介

HTTP(Hyper Text Transfer Protocal,超文本傳輸協議),簡單請求-響應協議,運行在TCP協議上,無狀態。它指定客戶端發送給服務器端的消息和得到的響應。請求和響應消息頭是ASCII碼;消息內容則類似MIME格式。

2.2.3 HTTP請求

客戶端發送到服務器端的請求消息。

  1. 請求行(Request Line)

請求方法、URI、HTTP協議/協議版本組成。

請求方法方法描述
GET請求頁面,並返回頁面內容,請求參數包含在URL中,提交數據最多1024byte
HEAD類似GET,只獲取報頭
POST提交表單或上傳文件,數據(含請求參數)包含在請求體中
PUT取代指定內容的文檔
DELETE刪除指定資源
OPTIONS查看服務器的性能
CONNECT服務器當作跳板,訪問其他網頁
TRACE回顯服務器收到的請求,用於測試或診斷
  1. 請求頭(Request Header)
請求頭示例說明
AcceptAccept: text/plain, text/html客戶端能夠接收的內容類型
Accept-charsetAccept-charset: iso-8859-5字符編碼集
Accept-EncodingAccept-Encoding: compress, gzip壓縮編碼類型
Accept-LanguageAccept-Language: en, zh語言
Accept-RangesAccept-Ranges: bytes子範圍字段
AuthorizationAuthorization: Basic dbXleoOEpePOetpoe2Ftyd==授權證書
Cache-ControlCache-Control: no-cache緩存機制
ConnectionConnection: close是否需要持久連接(HTTP1.1默認持久連接)
CookieCookie: $version=1; Skin=new;請求域名下的所有cookie值
Content-LengthContent-Length: 348內容長度
  1. 請求體(Request Body)

HTTP請求中傳輸數據的實體。

2.2.4 HTTP響應

服務器端返回給客戶端。

  1. 響應狀態碼(Response Status Code)

表示服務器的響應狀態。

狀態碼說明詳情
100繼續服務器收到部分請求,等待客戶端繼續提出請求
101切換協議請求者已要求服務器切換協議,服務器已確認並準備切換協議
200成功成功處理請求
201已創建服務器創建了新的資源
202已接受已接收請求,但尚未處理
203非授權信息成功處理請求,但返回信息來自另一個源
204無內容成功處理請求,無返回內容
205重置內容成功處理請求,內容重置
206部分內容成功處理部分內容
300多種選擇可執行多種操作
301永久移動永久重定向
302臨時移動暫時重定向
303查看其他位置重定向目標文檔應通過GET獲取
304未修改使用上次網頁資源
305使用代理應使用代理訪問
307臨時重定向臨時從其他位置響應
400錯誤請求無法解析
401未授權無身份驗證或驗證未通過
403禁止訪問拒絕
404未找到找不到
405方法禁用禁用指定方法
406不接受無法使用內容響應
407需要代理授權需要使用代理授權
408請求超時請求超時
409沖突完成請求時發生沖突
410已刪除資源永久刪除
411需要有效長度不接受標頭字段不含有效內容長度
412未滿足前提條件服務器未滿足某個前提條件
413請求實體過大超出能力
414請求URI過長網址過長,無法處理
415不支持類型格式不支持
416請求範圍不符頁面無法提供請求範圍
417未滿足期望值未滿足期望請求標頭字段
500服務器內部發生錯誤服務器錯誤
501未實現不具備功能
502錯誤網關收到無效響應
503服務不可用無法使用
504網關超時沒及時收到請求
505HTTP版本不支持不支持HTTP協議版本
  1. 響應頭(Response Headers)

包含服務器對請求的應答信息。

響應頭說明
Allow服務器支持的請求方法
Content-Encondig文檔編碼方法。
Content-Length內容長度,瀏覽器持久HTTP連接時需要
Content-Type文檔的MIME類型
DateGMT時間
Expires過期時間後,不再緩存
Last-Modified文檔最後改動時間。通過比較客戶端頭if-Modified-Since,可能返回304(Not Modified)。
Location客戶端應去哪裡提取文檔。
Refresh瀏覽器應刷新時間,秒
Server服務器名字
Set-Cookie設置頁面關聯Cookie
WWW-Authenticate客戶應在Authorization中提供授權信息,通常返回401。
  1. 響應體(Response Body)

HTTP請求返回的內容。 HTML,二進制數據,JSON文檔,XML文檔等。

2.2.5 URI與URL

  1. URI(Uniform Resource Identifier,統一資源標識符) 用來標識Web上每一種可用資源,概念。由資源的命名機制、存放資源的主機名、資源自身的名稱等組成。
  2. URL(Uniform Resource Locator,統一資源定位符) 用於描述網絡上的資源(描述信息資源的字符串),實現。使用統一格式,包括文件、服務器地址和目錄等。
scheme://host[:port#]/path/.../[?query-string][#anchor]
//協議(服務方式)
//主機域名或IP地址(可含端口號)
//具體地址,目錄和文件名等
  1. URN(Uniform Resource Name,統一資源名) 帶有名字的因特網資源,是URL的更新形式,不依賴位置,可減少失效鏈接個數。

2.2.6 HTTPS簡介

HTTPS(Hyper Text Transfer Protocol over SecureSocket Layer),在HTTP基礎上,通過傳輸加密和身份認證保證傳輸過程的安全型。HTTP + SSL/TLS。

TLS(Transport Layer Security,傳輸層安全性協議),及其前身SSL(Secure Socket Layer,安全套接字層),保障通信安全和數據完整性。

2.2.7 HTTP2簡介

  1. HTTP協議歷史
  • HTTP 0.9 只支持GET方法,不支持MIME類型和HTTP各種頭信息等。
  • HTTP 1.0 增加很多方法、各種HTTP頭信息,以及對多媒體對象的處理。
  • HTTP 1.1 主流HTTP協議,改善結構性缺陷,明確語義,增刪特性,支持更復雜的Web應用程序。
  • HTTP 2 優化性能,兼容HTTP 1.1語義,是二進制協議,頭部採用HPACK壓縮,支持多路復用、服務器推送等。
  1. HTTP 1.1與HTTP 2的對比
  • 頭信息壓縮 HTTP 1.1中,每一次發送和響應,都有HTTP頭信息。HTTP 2壓縮頭信息,減少帶寬。
  • 推送功能 HTTP 2之前,只能客戶端發送數據,服務器端返回數據。HTTP2中,服務器可以主動向客戶端發起一些數據傳輸(如css和png等),服務器可以並行發送html,css,js等數據。

2.2.8 Web應用程序的組成

  1. 處理器(hendler) 接收HTTP請求並處理。調用模板引擎生成html文檔返給客戶端。

MVC軟件架構模型

  • 模型(Model) 處理與業務邏輯相關的數據,以及封裝對數據的處理方法。有對數據直接訪問的權力,例如訪問數據庫。
  • 視圖(View) 實現有目的的顯示數據,一般沒有程序的邏輯。
  • 控制器(Controller) 組織不同層面,控制流程,處理用戶請求,模型交互等事件,並做出響應。

title模型Model控制器Controller視圖View瀏覽器模板引擎數據庫

  1. 模板引擎(template engine) 分離界面與數據(內容),組合模板(template)與數據(data),生成html文檔。 分為置換型(模板內容中特定標記替換)、解釋型和編譯型等。

模板template數據data模板引擎HTML文檔

2.3 net/http包

2.3.1 創建簡單服務器端

  1. 創建和解析HTTP服務器端
package main

import (
	"net/http"
)

func sayHello(w http.ResponseWriter, req *http.Request) {
	w.Write([]byte("Hello World"))
}

func main() {
	//註冊路由
	http.HandleFunc("/hello", sayHello)
	//開啟對客戶端的監聽
	http.ListenAndServe(":8080", nil)
}
http.HandleFunc()函數

//輸入參數:監聽端口號和事件處理器handler
http.ListenAndServe()函數

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}
package main

import (
	"net/http"
)

type Refer struct {
	handler http.Handler
	refer string
}

func (this *Refer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Referer() == this.refer {
		this.handler.ServeHTTP(w, r)
	} else {
		w.WriteHeader(403)
	}
}

func myHandler(w http.ResponseWriter, req *http.Request) {
	w.Write([]byte("this is handler"))
}

func hello(w http.ResponseWriter, req *http.Request) {
	w.Write([]byte("hello"))
}

func main() {
	referer := &Refer{
		handler: http.HandlerFunc(myHandler),
		refer: "www.shirdon.com",
	}
	http.HandleFunc("/hello", hello)
	http.ListenAndServe(":8080", referer)
}
  1. 創建和解析HTTPS服務器端
//證書文件路徑,私鑰文件路徑
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error
package main

import (
	"log"
	"net/http"
)

func handle(w http.ResponseWriter, r *http.Request) {
	log.Printf("Got connection: %s", r.Proto)
	w.Write([]byte("Hello this is a HTTP 2 message!"))
}

func main() {
	srv := &http.Server{Addr: ":8088", Handler: http.HandlerFunc(handle)}
	log.Printf("Serving on https://0.0.0.0:8088")
	log.Fatal(srv.ListenAndServeTLS("server.crt", "server.key"))
}

2.3.2 創建簡單的客戶端

//src/net/http/client.go
var DefaultClient = &Client{}

func Get(url string) (resp *Response, err error) {
	return DefaultClient.Get(url)
}

func (c *Client) Get(url string) (resp *Response, err error) {
	req, err := NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}
	return c.Do(req)
}

func Post(url, contentType string, body io.Reader) (resp *Response, err error) {
	return DefaultClient.Post(url, contentType, body)
}

func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) {
	req, err := NewRequest("POST", url, body)
	if err != nil {
		return nil, err
	}
	req.Header.set("Content-Type", contentType)
	return c.Do(req)
}
func NewRequest(method, url string, body io.Reader) (*Request, error)
//請求類型
//請求地址
//若body實現io.Closer接口,則Request返回值的Body字段會被設置為body值,並被Client的Do()、Post()和PostForm()方法關閉。
  1. 創建GET請求
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)

func main() {
	resp, err := http.Get("https://www.baidu.com")
	if err != nil {
		fmt.Println("err:", err)
	}
	closer := resp.Body
	bytes, err := ioutil.ReadAll(closer)
	fmt.Println(string(bytes))
}
  1. 創建POST請求
package main

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net/http"
)

func main() {
	url := "https://www.shirdon.com/comment/add"
	body := `{"userId": 1, "articleId": 1, "comment": 這是一條評論}`
	resp, err := http.Post(url, "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(body)))
	if err != nil {
		fmt.Println("err:", err)
	}
	bytes, err := ioutil.ReadAll(resp.Body)
	fmt.Println(string(bytes))
}
  1. 創建PUT請求
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
)

func main() {
	url := "https://www.shirdon.com/comment/update"
	payload := strings.NewReader(`{"userId": 1, "articleId": 1, "comment": 這是一條評論}`)
	req, _ := http.NewRequest("PUT", url, payload)
	req.Header.Add("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Println("err:", err)
	}
	defer res.Body.Close()
	bytes, err := ioutil.ReadAll(res.Body)
	fmt.Println(string(res))
	fmt.Println(string(bytes))
}
  1. 創建DELETE請求
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
)

func main() {
	url := "https://www.shirdon.com/comment/delete"
	payload := strings.NewReader(`{"userId": 1, "articleId": 1, "comment": 這是一條評論}`)
	req, _ := http.NewRequest("DELETE", url, payload)
	req.Header.Add("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Println("err:", err)
	}
	defer res.Body.Close()
	bytes, err := ioutil.ReadAll(res.Body)
	fmt.Println(string(res))
	fmt.Println(string(bytes))
}
  1. 請求頭設置
type Header map[string][]string
headers := http.Header{"token": {"feeowiwpor23dlspweh"}}
headers.Add("Accept-Charset", "UTF-8")
headers.Set("Host", "www.shirdon.com")
headers.Set("Location", "www.baidu.com")

2.4 html/template包

text/template處理任意格式的文本,html/template生成可對抗代碼注入的安全html文檔。

2.4.1 模板原理

  1. 模板和模板引擎 模板是事先定義好的不變的html文檔,模板渲染使用可變數據替換html文檔中的標記。模板用於顯示和數據分離(前後端分離)。模板技術,本質是模板引擎利用模板文件和數據生成html文檔。
  2. Go語言模板引擎
  • 模板文件後綴名通常為.tmpl和.tpl,UTF-8編碼
  • 模板文件中{{和}}包裹和標識傳入數據
  • 點號(.)訪問數據,{{.FieldName}}訪問字段
  • 除{{和}}包裹內容外,其他內容原樣輸出

使用: (1)定義模板文件 按照相應語法規則去定義。 (2)解析模板文件 創建指定模板名稱的模板對象

func New(name string) *Template

解析模板內容

func (t *Template) Parse(src string) (*Template, error)

解析模板文件

func ParseFiles(filenames...string) (*Template, error)

正則匹配解析文件,template.ParaeGlob(“a*”)

func ParseGlob(pattern string) (*Template, error)

(3)渲染模板文件

func (t *Template) Execute(wr io.Writer, data interface{}) error

//配合ParseFiles()函數使用,需指定模板名稱
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error

2.4.2 使用html/template包

  1. 第1個模板 template_example.tmpl
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>模板使用示例</title>
</head>
<body>
   <p>加油,小夥伴, {{ . }} </p>
</body>
</html>
package main

import (
	"fmt"
	"html/template"
	"net/http"
)

func helloHandleFunc(w http.ResponseWriter, r *http.Request) {
	// 1. 解析模板
	t, err := template.ParseFiles("./template_example.tmpl")
	if err != nil {
		fmt.Println("template parsefile failed, err:", err)
		return
	}
	// 2.渲染模板
	name := "我愛Go語言"
	t.Execute(w, name)
}

func main() {
	http.HandleFunc("/", helloHandleFunc)
	http.ListenAndServe(":8086", nil)
}
  1. 模板語法 模板語法都包含在{{和}}中間。
type UserInfo struct {
	Name string
	Gender string
	Age int
}

func sayHello(w http.ResponseWriter, r *http.Request) {
	tmpl, err := template.ParseFiles("./hello.html")
	if err != nil {
		fmp.Println("create template failed, err:", err)
		return
	}

	user := UserInfo {
		Name: "張三",
		Gender: "男",
		Age: 28,
	}
	tmpl.Execute(w, user)
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello</title>
</head>
<body>
   <p>Hello {{.Name}}</p>
   <p>性別:{{.Gender}}</p>
   <p>年齡:{{.Age}}</p>
</body>
</html>

常用語法:

  • 注釋
{{/* 這是一個注釋,不會解析 */}}
  • 管道(pipeline) 產生數據的操作,{{.Name}}等。支持|鏈接多個命令,類似UNIX下管道。
  • 變量 變量捕獲管道的執行結果。
$variable := pipeline
  • 條件判斷
{{if pipeline}} T1 {{end}}
{{if pipeline}} T1 {{else}} T0 {{end}}
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
  • range關鍵字
{{range pipeline}} T1 {{end}}
{{range pipeline}} T1 {{else}} T0 {{end}}
package main

import (
	"log"
	"os"
	"text/template"
)

func main() {
	//創建一個模版
	rangeTemplate := `
{{if .Kind}}
{{range $i, $v := .MapContent}}
{{$i}} => {{$v}} , {{$.OutsideContent}}
{{end}}
{{else}}
{{range .MapContent}}
{{.}} , {{$.OutsideContent}}
{{end}}    
{{end}}`

	str1 := []string{"第一次 range", "用 index 和 value"}
	str2 := []string{"第二次 range", "沒有用 index 和 value"}

	type Content struct {
		MapContent     []string
		OutsideContent string
		Kind           bool
	}
	var contents = []Content{
		{str1, "第一次外面的內容", true},
		{str2, "第二次外面的內容", false},
	}

	// 創建模板並將字符解析進去
	t := template.Must(template.New("range").Parse(rangeTemplate))

	// 接收並執行模板
	for _, c := range contents {
		err := t.Execute(os.Stdout, c)
		if err != nil {
			log.Println("executing template:", err)
		}
	}
}
/*
//輸出
0 => 第一次 range, 第一次外面的內容
1 => 用 index 和 value, 第一次外面的內容

第二次 range, 第二次外面的內容
沒有用 index 和 value, 第二次外面的內容
*/
  • with關鍵字
{{with pipeline}} T1 {{end}}
{{with pipeline}} T1 {{else}} T0 {{end}}
  • 比較函數 比較函數只適用於基本函數(或重定義的基本類型,如type Banance float32),整數和浮點數不能相互比較。 布爾函數將任何類型的零值視為假。 只有eq可以接受2個以上參數。
{{eq arg1 arg2 arg3}}
eq
ne
lt
le
gt
ge
  • 預定義函數
函數名功能
and返回第1個空參數或最後一個參數,所有參數都執行。and x y等價於if x then y else x
or返回第1個非空參數或最後一個參數,所有參數都執行。and x y等價於if x then y else x
not
len長度
indexindex y 1 2 3, index[1][2][3]
printfmt.Sprint
printffmt.Sprintf
printlnfmt.Sprintln
htmlhtml逸碼等價表示
urlquery可嵌入URL查詢的逸碼等價表示
jsJavaScript逸碼等價表示
callcall func a b, func(a, b);1或2個返回值,第2個為error,非nil會中斷並返回給調用者。
  • 自定義函數 模板對象t的函數字典加入funcMap內的鍵值對。funcMap某個值不是函數類型,或該函數類型不符合要求,會panic。返回*Template便於鏈式調用。
func (t *Template) Funcs(funcMap FuncMap) *Template

FuncMap映射函數要求1或2個返回值,第2個為error,非nil會中斷並返回給調用者。

type FuncMap map[string]interface{}
package main

import (
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
)

func Welcome() string { //沒參數
	return "Welcome"
}

func Doing(name string) string { //有參數
	return name + ", Learning Go Web template "
}

func sayHello(w http.ResponseWriter, r *http.Request) {
	htmlByte, err := ioutil.ReadFile("./funcs.html")
	if err != nil {
		fmt.Println("read html failed, err:", err)
		return
	}
	// 自定義一個匿名模板函數
	loveGo := func() (string) {
		return "歡迎一起學習《Go Web編程實戰派從入門到精通》"
	}
	// 採用鏈式操作在Parse()方法之前調用Funcs添加自定義的loveGo函數
	tmpl1, err := template.New("funcs").Funcs(template.FuncMap{"loveGo": loveGo}).Parse(string(htmlByte))
	if err != nil {
		fmt.Println("create template failed, err:", err)
		return
	}
	funcMap := template.FuncMap{
		//在FuncMap中聲明相應要使用的函數,然後就能夠在template字符串中使用該函數
		"Welcome": Welcome,
		"Doing":   Doing,
	}
	name := "Shirdon"
	tmpl2, err := template.New("test").Funcs(funcMap).Parse("{{Welcome}}<br/>{{Doing .}}")
	if err != nil {
		panic(err)
	}

	// 使用user渲染模板,並將結果寫入w
	tmpl1.Execute(w, name)
	tmpl2.Execute(w, name)
}

func main() {
	http.HandleFunc("/", sayHello)
	http.ListenAndServe(":8087", nil)
}

funcs.html

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<meta http-equiv="X-UA-Compatible" content="ie=edge">
		<title>tmpl test</title>
	</head>
	<body>
		<h1>{{loveGo}}</h1>
	</body>
</html>
  • 模板嵌套 可以通過文件嵌套和define定義
{{define "name"}} T {{end}}
{{template "name"}}
{{template "name" pipeline}}
{{block "name" pipeline}} T {{end}}
//等價於
{{define "name"}} T {{end}}
{{template "name" pipeline}}

t.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>tmpl test</title>
</head>
<body>
<h1>測試嵌套template語法</h1>
<hr>
{{template "ul.html"}}
<hr>
{{template "ol.html"}}
</body>
</html>
{{define "ol.html"}}
<h1>這是ol.html</h1>
<ol>
    <li>I love Go</li>
    <li>I love java</li>
    <li>I love c</li>
</ol>
{{end}}

ul.html

<ul>
    <li>注釋</li>
    <li>日誌</li>
    <li>測試</li>
</ul>
package main

import (
	"fmt"
	"html/template"
	"net/http"
)

//定義一個UserInfo結構體
type UserInfo struct {
	Name string
	Gender string
	Age int
}

func tmplSample(w http.ResponseWriter, r *http.Request) {
	tmpl, err := template.ParseFiles("./t.html", "./ul.html")
	if err != nil {
		fmt.Println("create template failed, err:", err)
		return
	}
	user := UserInfo{
		Name:   "張三",
		Gender: "男",
		Age:    28,
	}
	tmpl.Execute(w, user)
	fmt.Println(tmpl)
}

func main() {
	http.HandleFunc("/", tmplSample)
	http.ListenAndServe(":8087", nil)
}

Golang 單步除錯利器 — Delve

Golang 是一個靜態語言,雖然也有支援 GDB,但官方也有說,若單純使用內建的 toolchain,推薦使用 Delve 這個工具,因為 GDB 對於 Go 的 stack 管理、線程或執行時期的環境,並沒有支援的很完善,有時甚至會看到錯誤的狀態。

Delve 也是單步執行工具,和 GDB 很像,但是他更方便安裝,本身也是 go 的 package 之一,安裝方法如下:

Mac 要先安裝編譯用的 toolchain:

$ xcode-select --install
$ go get -u github.com/derekparker/delve/cmd/dlv

對於 Windows 和 Linux 系統,只需要執行 go get即可:

https://github.com/go-delve/delve/tree/master/Documentation/installation

$ go install github.com/go-delve/delve/cmd/dlv@v1.7.3

記得,所有平臺都要先將 $GOPATH/bin 加入系統環境 PATH變數,這樣才找得到執行檔。

除錯

首先,我們用下面的程式當作範例:

package main

import (
    "fmt"
)

func demo(s string, x int) string {
    ret := fmt.Sprintf("This is a demo, your input is: %s %d", s, x)
    return ret
}

func main() {
    s := "string"
    i := 1111
    fmt.Println(demo(s, i))
}

存檔成為 delve-demo.go

接著,在 console 使用 dlv debug <filename>將 delve 跑起來:

$ dlv debug delve-demo.go --check-go-version=false

你會看到下面的訊息:

$ dlv debug delve-demo.go                                                                                                                                  system
Type 'help' for list of commands.
(dlv) 

但其實程式還沒真的跑起來,此時可以先設定中斷點,再來跑程式。

設定中斷點: 使用 <package>.<function>或是 <filename>:<line number>的格式:

# 方法 1
(dlv) break main.main
Breakpoint 1 set at 0x10b0958 for main.main() ./delve-demo.go:12# 方法 2
(dlv) break delve-demo.go:7
Breakpoint 2 set at 0x10b0758 for main.demo() ./delve-demo.go:7

接著使用 c 或是 continue讓程式跑起來,你就會看到 dlv 停在中斷點上:

(dlv) b main.main
Breakpoint 1 set at 0x10b0958 for main.main() ./delve-demo.go:12
(dlv) c
> main.main() ./delve-demo.go:12 (hits goroutine(1):1 total:1) (PC: 0x10b0958)
     7: func demo(s string, x int) string {
     8:  ret := fmt.Sprintf("This is a demo, your input is: %s %d", s, x)
     9:  return ret
    10: }
    11:
=>  12: func main() {
    13:  s := "string"
    14:  i := 1111
    15:  fmt.Println(demo(s, i))
    16: }
(dlv)

其他使用方式就和 GDB 雷同,下面把比較常用的指令列出來:

單部執行: nnext跳進去函式 (step in): sstep跳出函式 (step out): stepout看函式引數: args 例如:

(dlv) args
s = "string"
x = 1111

印出參數或表達式: print <參數> 例如:

(dlv) p x
1111
(dlv) p x+5
1116
(dlv) p x != 5
true

印出目前所有的 goroutine:

(dlv) goroutines
[4 goroutines]
* Goroutine 1 - User: ./delve-demo.go:9 main.demo (0x10b08e9) (thread 2350822)
  Goroutine 2 - User: /usr/local/Cellar/go/1.10.3/libexec/src/runtime/proc.go:292 runtime.gopark (0x102c209)
  Goroutine 3 - User: /usr/local/Cellar/go/1.10.3/libexec/src/runtime/proc.go:292 runtime.gopark (0x102c209)
  Goroutine 4 - User: /usr/local/Cellar/go/1.10.3/libexec/src/runtime/proc.go:292 runtime.gopark (0x102c209)

更多詳細的指令可以參考 github 說明

testing 除錯

如果要在跑 go test 的時候除錯也很容易,要跑全部的 test case 只要執行

dlv test -- -test.v
# 如同
go test ./...

或是隻執行單個測資:

dlv test -- -test.run <test function>
# 如同
go test -run <test function>

Happy debugging!

從一知半解到略懂 Go modules

一直以來沒有好好去詳讀 go modules 的文件,所以都覺得對 go modules 只是一知半解。這次花了些時間看了關於 go modules 的相關文件,並實際寫個小範例體驗,最後整理成本文分享。

本文環境

  • macOS 10.15
  • Go 1.13

引言

先前 Golang - 從 Hello World 認識 GOPATH 一文中,我們認識了 GOPATH 的作用,然而 GOPATH 會讓我們的專案程式碼與其他相依的程式碼一起存在 $GOPATH/src 資料夾底下,相較於其他程式語言而言,使用上較不直覺,也欠缺相依性管理的功能。

Go 1.11 之後提供 go modules 讓我們可以不需要把專案程式碼放在 $GOPATH/src 中開發,此外還能管理套件相依性,相當便利。

Go modules 初體驗

首先設置好 GOPATH 之後,先在 $GOPATH/src 之外,新增 1 個資料夾存放專案程式:

$ export GOPATH=/path/to/goworkspace
$ mkdir myproject
$ cd myproject

接著用以下指令新增 Go module:

$ go mod init github.com/username/myproject

p.s. github.com/username/myproject 可以換成任意字串,因為個人希望將 Go module 放置於 GitHub, 因此將模組名稱設定為 github.com/username/myproject

上述指令成功後,將會看到資料夾內出現 1 個檔案 go.mod :

module github.com/username/myproject

go 1.13

go.mod 用來紀錄 Go module 的名稱與所使用的 Go 版本,以及相依的 Go modules, 該檔案是 Go module 必備的檔案

再來新增 2 個資料夾,以及 2 個 .go 檔,建立範例所需要的環境:

$ mkdir greeting cli
$ touch greeting/greeting.go cli/say.go

進行至此, myproject 的資料夾結構應如下所示:

.
├── cli
│   └── say.go
├── go.mod
└── greeting
    └── greeting.go

最後將 greeting.gosay.go 填入以下程式碼。 greeting.go 是 1 個簡單的 package, 用以列印所傳入的字串;而 say.go 則是用以呼叫 greeting.go package 所提供的函示。

greeting.go 的內容:

package greeting

import "fmt"

func Say(s string) {
    fmt.Println(s)
}

say.go 的內容:

package main

import "github.com/username/myproject/greeting"

func main() {
    greeting.Say("Hello")
}

最後,試著編譯一次,正常的話不會有任何錯誤訊息:

$ go build ./...

至此,我們已經利用 go modules 成功地將 Go 專案移出 $GOPATH/src 囉!

p.s. 如果把 go.mod 刪除的話,就會發現類似以下的錯誤,這是由於 go 找不到 go.mod 因此轉而至 $GOPATH 尋找相關的 go package 的緣故:

cli/say.go:3:8: cannot find package "github.com/username/myproject/greeting" in any of:
    /usr/local/go/src/github.com/username/myproject/greeting (from $GOROOT)
    $GOPATH/src/github.com/username/myproject/greeting (from $GOPATH)

使用 go modules 進行套件相依性管理

Go modules 提供的另一個方便的功能則是套件相依性管理,接下來實際透過以下指令安裝套件試試:

$ go get github.com/fatih/color

安裝成功之後,可以再看一次 go.mod ,會發現多了 1 行 require github.com/fatih/color v1.9.0 :

module github.com/username/myproject

go 1.13

require github.com/fatih/color v1.9.0

require github.com/fatih/color v1.9.0 目前的 Go 專案需要 v1.9.0 版的 github.com/fatih/color

p.s. go modules 使用的版本號規則是semantic version , 有興趣的話,可以詳閱該文件

有時候我們可能會需要使用指定版本的 package, 這時候可以在 package 尾端加上 @版本號 ,例如以下指定使用 v1.8.0 的 github.com/fatih/color

$ go get github.com/fatih/color@v1.8.0

安裝完成後,再看一次 go.mod 會發現除了 github.com/fatih/color 版本變為 v1.8.0 之外,又多了 2 個 //indirect 的 go packages:

module github.com/username/myproject

go 1.13

require (
    github.com/fatih/color v1.8.0
    github.com/mattn/go-colorable v0.1.4 // indirect
    github.com/mattn/go-isatty v0.0.11 // indirect
)

//indirect 指的是被相依的套件所使用的 packages:

The indirect comment indicates a dependency is not used directly by this module, only indirectly by other module dependencies.

另一種常見情況是我們可能會指定 package 到某個 commit id, 這時候就能夠使用 pseudo-version ,例如 v0.0.0-20170915032832-14c0d48ead0c 就是 1 個指定使用 20170915032832-14c0d48ead0c commit 的 pseudo-version.

pseudo-version , which is the go command’s version syntax for a specific untagged commit.

接著,可以再把 greeting.gosay.go 改為以下形式,使用剛剛所安裝的 package 。

greeting.go 的內容:

package greeting

import "fmt"

import "github.com/fatih/color"

func Say(s string) {
    fmt.Println(s)
}

func SayWithColor(s string) {
    color.Red(s)
}

say.go 的內容:

package main

import "github.com/username/myproject/greeting"

func main() {
    greeting.Say("Hello")
    greeting.SayWithColor("World")
}

go.mod 的 replace 語法

go.mod 還提供 replace 語法,能夠讓我們取代指定的套件,例如 replace github.com/fatih/color => ../mycolor 代表至 ../mycolor 資料夾中載入 github.com/fatih/color package, 例如以下的 go.mod :

module github.com/username/myproject

go 1.13

require (
    github.com/fatih/color v1.8.0
    github.com/mattn/go-colorable v0.1.4 // indirect
    github.com/mattn/go-isatty v0.0.11 // indirect
)

replace github.com/fatih/color => ../mycolor

除了直接編輯 go.mod 之外,也可以用以下指令:

$ go mod edit -replace github.com/fatih/color=../mycolor

replace 能夠讓我們輕易地將特定 package 重新定位到特定路徑下,除了能夠方便修改之外,也能夠讓我們更輕鬆地測試 package 不同版本的行為等等,值得注意的是特定路徑下的 package 也必須有 go.mod 檔才行

../mycolor 是代表在 go.mod 檔案的所在目錄的上一層,所以可以先切換至上一層目錄後,再次下載 https://github.com/fatih/color 試試:

$ cd ../
$ git clone https://github.com/fatih/color mycolor

此時的資料夾結構應該會類似以下:

.
├── mycolor
│   ├── LICENSE.md
│   ├── README.md
│   ├── color.go
│   ├── color_test.go
│   ├── doc.go
│   ├── go.mod
│   ├── go.sum
│   └── vendor
├── myproject
│   ├── cli
│   ├── go.mod
│   ├── go.sum
│   └── greeting
└── pkg

接著回到 myproject 試著編譯看看,正常的話就不會出現任何訊息:

$ cd myproject
$ go build ./...

如此代表成功體驗 replace 的功用了!

結語

以上就是關於 go modules 的一些解說與用法,還有很多細節可以詳閱官方文件,相信大家閱讀之後都可以有不少收獲!

Happy coding!

References

https://blog.golang.org/using-go-modules

https://golang.org/ref/mod

出處

https://myapollo.com.tw/zh-tw/golang-go-module-tutorial/

Go modules

避免重複造輪子 ,所以今天要介紹的就是怎麼使用 Go modules 引用外部的 library

初始化

這邊要使用 go mod init <project-name> 進行初始化(類似 npm init),完成後會多一個檔案 go.mod(就像 Nodejs 中的 package.json),因為現在都還沒安裝任何依賴所以 go.mod 裡面只有一行 module go-phishing

 go mod init go-phishing

安裝、使用 dependencies

go get github.com/sirupsen/logrus
package main

import (
  "github.com/sirupsen/logrus"
)

func main() {
  logrus.SetLevel(logrus.TraceLevel)

  logrus.Trace("trace msg")
  logrus.Debug("debug msg")
  logrus.Info("info msg")
  logrus.Warn("warn msg")
  logrus.Error("error msg")
  logrus.Fatal("fatal msg")
  logrus.Panic("panic msg")
}
go run main.go

編譯完再看一下 go.mod 裡面就有 logrus 了,跟 Nodejs 的 package.json 長得很像

module go-phishing

go 1.18

require (
	github.com/sirupsen/logrus v1.8.1 // indirect
	golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
)

go.sum

編譯完之後除了 go.mod 之外還會多出一個檔案 go.sum,因為 logrus 也會用到某些 package,裡面記錄的是所有用到的 package 版本,類似 Nodejs 的 package-lock.json,如果你有在使用 git 之類的版本控制系統,記得要在 commit 時把它加進去

github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

Go Modules 指令介紹

Usage:go mod <command> [arguments]The commands are:download    // 將依賴全部下載到本機中,位置為 $GOPATH/pkg/mod/cache
edit        // 編輯 go.mod 例如鎖定某個依賴的版本
graph       // 列出專案中哪一個部分使用了某個依賴
init        // 建立 go.mod
tidy        // 增加遺失的依賴,移除未使用的依賴
vendor      // 將既有的 go.mod 依賴全部存在 /vendor 底下
verify      // 驗證本地依賴依然符合 go.sum 
why         // 解釋某個依賴為何存在在 go.mod 中,誰使用了它

如何在一個新的專案使用 Go Modules?

以下以 OSX 為例

// 先確認 Go 的版本已經在 1.11 以上
$ brew upgrade go$ mkdir gomod-test // 請確定當前位置在 $GOPATH 以外的地方
$ cd gomod-test

其實不一定要在 $GOPATH 以外的地方,只是當前 Go Modules 還在實驗階段,如果是在 $GOPATH 的專案,預設依然會照舊有的 vendor 機制,除非將環境變數 GO111MODULE 該為 on 來強制開啟,但既然 Go Modules 一個重要的性質是去除 $GOPATH ,就讓我們試試看在其他地方開專案吧!

接著初始化 Go Modules

$ go mod init github.com/hieven/gomod-test

便會看到專案底下出現 go.mod 的檔案,而這也是最重要的檔案,之後會紀錄每一個 dependency 以及版本。現在應該長得像這樣

// go.mod
module github.com/hieven/gomod-test

最後讓我們做一個簡單的 hello world

// main.go
package mainimport "fmt"func main() {
  fmt.Println("hello world")
}

此時還沒有任何改變,但接著我們嘗試加入一個 dependency

package mainimport "fmt"
import "github.com/gofrs/uuid"func main() {
  uuid, _ := uuid.NewV4()
  
  fmt.Println("hello world", uuid)
}

接著運行程式 $ go run main.go 會發現程式神奇的運作了

$ hello world 3f99abff-8404-42ec-b9f6-5fa165e5d447

再來檢查 go.mod ,會發現已經多了一個 dependency

module github.com/hieven/gomod-testrequire (
  github.com/gofrs/uuid v3.1.0+incompatible
)

原因是 Go Modules 不僅僅是一個 go mod xxx 的工具而已,同時也整合了既有的 go getgo rungo buildgo test ,每當這些指令運行時,都會去檢查整個 project 底下新的 dependency 並自動更新到 go.mod 底下

既有的專案如何遷移至 Go Modules?

剛好在既有的 project 中分別有用 glide 以及 govendor,所以剛好都試過這兩個的遷移方法,其實非常簡單。只要到 project 底下執行

$ go mod init

便自動會去讀 glide.yaml 或是 vendor/vendor.json 並產生一個 go.mod 。個人目前還沒有遇到問題

如果有興趣的人可以參考我在 go-instagram 中的一個 PR,便是把 glide 轉移成 Go Modules

此外,可以試試看執行

$ go mod tidy

來移除沒使用的依賴,我自己的私人專案在使用 tidy 之後,成功移除了好幾個呢!

如何讓 Travis 能使用 Go Modules?

基本上現在的 Travis 也有支援 Go 1.11 了,所以 Go Modules 自然而然就有了。

唯一要特別注意的是, Travis 底下預設把 project 放在 $GOPATH 底下,所以要在 env 中把 Go Modules 打開才行

具體設定就是要注意這兩行

// .travis.ymlgo:
  - "1.11"env:
  - GO111MODULE=on

Golang大殺器之跟蹤剖析trace

在 Go 中有許許多多的分析工具,在之前我有寫過一篇 《Golang 大殺器之性能剖析 PProf》 來介紹 PProf,如果有小夥伴感興趣可以去我博客看看。

但單單使用 PProf 有時候不一定足夠完整,因為在真實的程序中還包含許多的隱藏動作,例如 Goroutine 在執行時會做哪些操作?執行/阻塞了多長時間?在什麼時候阻止?在哪裡被阻止的?誰又鎖/解鎖了它們?GC 是怎麼影響到 Goroutine 的執行的?這些東西用 PProf 是很難分析出來的,但如果你又想知道上述的答案的話,你可以用本文的主角 go tool trace 來打開新世界的大門。目錄如下:

初步瞭解

import (
"os"
"runtime/trace"
)
func main() {
trace.Start(os.Stderr)
defer trace.Stop()
ch := make(chan string)
go func() {
ch <- "EDDYCJY"
}()
<-ch
}

生成跟蹤文件:

$ go run main.go 2> trace.out

啟動可視化界面:

$ go tool trace trace.out
2019/06/22 16:14:52 Parsing trace...
2019/06/22 16:14:52 Splitting trace...
2019/06/22 16:14:52 Opening browser. Trace viewer is listening on http://127.0.0
  • View trace:查看跟蹤
  • Goroutine analysis:Goroutine 分析
  • Network blocking profile:網絡阻塞概況
  • Synchronization blocking profile:同步阻塞概況
  • Syscall blocking profile:系統調用阻塞概況
  • Scheduler latency profile:調度延遲概況
  • User defined tasks:用戶自定義任務
  • User defined regions:用戶自定義區域
  • Minimum mutator utilization:最低 Mutator 利用率

Scheduler latency profile

在剛開始查看問題時,除非是很明顯的現象,否則不應該一開始就陷入細節,因此我們一般先查看 “Scheduler latency profile”,我們能通過 Graph 看到整體的調用開銷情況,如下:

Golang大殺器之跟蹤剖析trace

演示程序比較簡單,因此這裡就兩塊,一個是 trace 本身,另外一個是 channel 的收發。

Goroutine analysis

第二步看 “Goroutine analysis”,我們能通過這個功能看到整個運行過程中,每個函數塊有多少個有 Goroutine 在跑,並且觀察每個的 Goroutine 的運行開銷都花費在哪個階段。如下:

Golang大殺器之跟蹤剖析trace

通過上圖我們可以看到共有 3 個 goroutine,分別是 runtime.mainruntime/trace.Start.func1main.main.func1,那麼它都做了些什麼事呢,接下來我們可以通過點擊具體細項去觀察。如下:

Golang大殺器之跟蹤剖析trace

同時也可以看到當前 Goroutine 在整個調用耗時中的佔比,以及 GC 清掃和 GC 暫停等待的一些開銷。如果你覺得還不夠,可以把圖表下載下來分析,相當於把整個 Goroutine 運行時掰開來看了,這塊能夠很好的幫助我們對 Goroutine 運行階段做一個的剖析,可以得知到底慢哪,然後再決定下一步的排查方向。如下:

名稱含義耗時
Execution Time執行時間3140ns
Network Wait Time網絡等待時間0ns
Sync Block Time同步阻塞時間0ns
Blocking Syscall Time調用阻塞時間0ns
Scheduler Wait Time調度等待時間14ns
GC SweepingGC 清掃0ns
GC PauseGC 暫停0ns

View trace

在對當前程序的 Goroutine 運行分佈有了初步瞭解後,我們再通過 “查看跟蹤” 看看之間的關聯性,如下:

Golang大殺器之跟蹤剖析trace

這個跟蹤圖粗略一看,相信有的小夥伴會比較懵逼,我們可以依據註解一塊塊查看,如下:

  1. 時間線:顯示執行的時間單元,根據時間維度的不同可以調整區間,具體可執行 shift + ? 查看幫助手冊。
  2. 堆:顯示執行期間的內存分配和釋放情況。
  3. 協程:顯示在執行期間的每個 Goroutine 運行階段有多少個協程在運行,其包含 GC 等待(GCWaiting)、可運行(Runnable)、運行中(Running)這三種狀態。
  4. OS 線程:顯示在執行期間有多少個線程在運行,其包含正在調用 Syscall(InSyscall)、運行中(Running)這兩種狀態。
  5. 虛擬處理器:每個虛擬處理器顯示一行,虛擬處理器的數量一般默認為系統內核數。
  6. 協程和事件:顯示在每個虛擬處理器上有什麼 Goroutine 正在運行,而連線行為代表事件關聯。

Golang大殺器之跟蹤剖析trace

點擊具體的 Goroutine 行為後可以看到其相關聯的詳細信息,這塊很簡單,大家實際操作一下就懂了。文字解釋如下:

  • Start:開始時間
  • Wall Duration:持續時間
  • Self Time:執行時間
  • Start Stack Trace:開始時的堆棧信息
  • End Stack Trace:結束時的堆棧信息
  • Incoming flow:輸入流
  • Outgoing flow:輸出流
  • Preceding events:之前的事件
  • Following events:之後的事件
  • All connected:所有連接的事件

View Events

我們可以通過點擊 View Options-Flow events、Following events 等方式,查看我們應用運行中的事件流情況。如下:

Golang大殺器之跟蹤剖析trace

通過分析圖上的事件流,我們可得知這程序從 G1 runtime.main 開始運行,在運行時創建了 2 個 Goroutine,先是創建 G18 runtime/trace.Start.func1,然後再是 G19 main.main.func1 。而同時我們可以通過其 Goroutine Name 去了解它的調用類型,如:runtime/trace.Start.func1 就是程序中在 main.main 調用了 runtime/trace.Start 方法,然後該方法又利用協程創建了一個閉包 func1 去進行調用。

Golang大殺器之跟蹤剖析trace

在這裡我們結合開頭的代碼去看的話,很明顯就是 ch 的輸入輸出的過程了。

結合實戰

今天生產環境突然出現了問題,機智的你早已埋好 _ "net/http/pprof" 這個神奇的工具,你麻利的執行了如下命令:

  • curl http://127.0.0.1:6060/debug/pprof/trace?seconds=20 > trace.out
  • go tool trace trace.out

View trace

你很快的看到了熟悉的 List 界面,然後不信邪點開了 View trace 界面,如下:

Golang大殺器之跟蹤剖析trace

完全看懵的你,穩住,對著合適的區域執行快捷鍵 W 不斷地放大時間線,如下:

Golang大殺器之跟蹤剖析trace

經過初步排查,你發現上述絕大部分的 G 竟然都和 google.golang.org/grpc.(*Server).Serve.func 有關,關聯的一大串也是 Serve 所觸發的相關動作。

Golang大殺器之跟蹤剖析trace

這時候有經驗的你心裡已經有了初步結論,你可以繼續追蹤 View trace 深入進去,不過我建議先鳥瞰全貌,因此我們再往下看 “Network blocking profile” 和 “Syscall blocking profile” 所提供的信息,如下:

Network blocking profile

Golang大殺器之跟蹤剖析trace

Syscall blocking profile

Golang大殺器之跟蹤剖析trace

通過對以上三項的跟蹤分析,加上這個洩露,這個阻塞的耗時,這個涉及的內部方法名,很明顯就是哪位又忘記關閉客戶端連接了,趕緊改改改。

總結

通過本文我們習得了 go tool trace 的武林祕籍,它能夠跟蹤捕獲各種執行中的事件,例如 Goroutine 的創建/阻塞/解除阻塞,Syscall 的進入/退出/阻止,GC 事件,Heap 的大小改變,Processor 啟動/停止等等。

希望你能夠用好 Go 的兩大殺器 pprof + trace 組合,此乃排查好搭檔,誰用誰清楚,即使他並不萬能。

程序(進程)、執行緒(線程)、協程,傻傻分得清楚!

要成為一個優秀的軟體工程師,進程(process)、線程(thread)是一定要搞懂與掌握的知識點,不僅是因為它們是電腦科學根本的知識,更是因為懂得在適當的時機善用它們,可以增進程式的執行效率,也就是提升效能。

程式 Program

在瞭解進程與線程之前,得先談談 program 這個東西,其實所謂的 program 就是工程師撰寫的程式碼的集合,例如 Line、 Chrome 就個別是 program,而他們的特點是還沒有被執行,因此也就還沒有被載入至記憶體中,而是存放在次級儲存裝置中。

進程 Process

Process 進程則是指被執行且載入記憶體的 program**。**Process 也是 OS 分配資源的最小單位,可以從 OS 得到如 CPU Time、Memory…等資源,意思是這個 process 在運行時會消耗多少 CPU 與記憶體。文章一開始放了一張 MacOS 活動監視器的截圖,相信不管是使用哪種作業系統的讀者都有看過類似的介面,而監視器中列出的是你的電腦正在執行的應用程式,而它們其實就是一個個 process,可以從圖片中看出每一個 process 消耗的 CPU、CPU 時間與每個 process 的獨立 ID (PID)。

進程的優缺點與小結

  • 優點:每個進程有自己獨立的系統資源分配空間,不同進程之間的資源不共享,因此不需要特別對進程做互斥存取的處理。
  • 缺點:建立進程以及進程的上下文切換(Context Switch)都較消耗資源,進程間若有通訊的需求也較為複雜。

小結:程式 (Program)是寫好尚未執行的 code,程式被執行後才會變成進程 (Process)。

線程 Thread

線程可以想像成存在於 process 裡面,而一個進程裡至少會有一個線程,前面有說 process 是 OS 分配資源的最小單位,而 thread 則是作業系統能夠進行運算排程的最小單位,也就是說實際執行任務的並不是進程,而是進程中的線程,一個進程有可能有多個線程,其中多個線程可以共用進程的系統資源,可以把進程比喻為一個工廠,線程則是工廠裡面的工人,負責任務的實際執行。

MultiProcessing 多進程 & MultiThreading 多線程

這兩個概念我想繼續利用工廠與工人的比喻會比較好理解與記憶。

Multiprocessing 好比建立許多工廠(通常會取 CPU 的數量),每個工廠中會分配ㄧ名員工(thread)執行工作,因此優勢在於同一時間內可以完成較多的事。

Multithreading 則是將許多員工聚集到同一個工廠內,它的優勢則是有機會讓相同的工作在比較短的時間內完成。

多線程的 Race Condition

剛剛有提到在多執行緒中 (Multithreading),不同 thread 是可以共享資源的,而若兩個 thread 若同時存取或改變全域變數,可能會發生同步 (Synchronization) 問題。若執行緒之間互搶資源,則可能產生死結 (Deadlock),因此使用多線程時必須特別注意避免這些狀況發生。

Concurrent & Parallel 並發與並行

這兩個是許多人容易誤解的概念,然而透過上面的圖就可以一目瞭然,Parallel 並行是利用多個 CPU 達到同時並行處理任務的需求(也就是同一個時間點有許多任務在同時執行),Concurrent 則是許多任務在爭搶同一個 CPU 的資源,因此一個時間點只會有一個任務正在執行,只是因為切換非常快,使用者通常不會感覺到任務實際上一直在切換。

協程 Coroutine

大部分的文章討論的都是 process 與 thread 的概念,直到最近在學 golang,碰到了 goroutine,才知道原來還有 coroutine 協程的存在。

先講重點

協程是一種使用者態的輕量級執行緒,協程的排程完全由使用者控制。

可以想像進程中有線程,而線程中則又有協程。*而協程的調度完全由用戶控制*,協程也會有自己的 registers、context、stack 等等,並且由協程的調度器來控制目前由哪個協程執行,哪個協程要被 block 。process 及 thread 的調度,是由 CPU 內核去進行調度,而協程卻不ㄧ樣,OS 甚至不知道協程的存在,如果 coroutine 被卡住,則會在用戶端直接切換另外一個 coroutine 給此 thread 繼續執行,這樣其他 coroutine 就不會被block住,讓資源能夠有效的被利用,藉此實現 Concurrent 的概念。

相較於建立一個線程需要花費 MB 等級的記憶體,建立一個協程可以壓到 KB 等級,協程間的切換也絕對快於線程間的切換。

如果有興趣,真的非常推薦去學 Go 語言的 goroutine,相信你也會被它的強大給深深吸引的。

小結

透過這篇文章快速帶過進程、線程、協程的概念,然而實際上要使用多進程、多線程開發時要考慮的因素真的很多,一不小心可能會造成上面提過的 race condition 或是性能不升反降的囧境,因此在使用上仍須經過謹慎考慮與效能驗證囉!

補充:名詞對照

其實本篇文章所提及的進程與線程都是對岸的用語,為了避免讀者誤解,下面整理了一些臺灣與對岸的名詞對照表:

process:

  • 臺灣:程序、處理程序
  • 對岸:進程

thread:

  • 臺灣:執行緒
  • 對岸:線程

concurrent:

  • 臺灣:並行
  • 大陸:並發

parallel:

  • 臺灣:平行
  • 大陸:並行

可以看到兩個地區對並行的定義是不一樣的,因此建議讀者已英文記比較不會搞混喔~

出處

https://oldmo860617.medium.com/%E9%80%B2%E7%A8%8B-%E7%B7%9A%E7%A8%8B-%E5%8D%94%E7%A8%8B-%E5%82%BB%E5%82%BB%E5%88%86%E5%BE%97%E6%B8%85%E6%A5%9A-a09b95bd68dd

Go 的並發:Goroutine 與 Channel 介紹

Goroutine 像是 Go 語言的 thread, 使 Go 建立多工處理, 搭配 Channel 使 Goroutine 操作簡單化, 本文介紹 Goroutine 及 Channel 的使用方式。

範例程式碼可以在 golang-concurrency-example 中找到,每個程式第一行可以找到其範例檔名。

單執行緒

在單執行緒下,每行程式碼都會依照順序執行。

// single-thread.go
func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {g
    say("world")
    say("hello")
}
world
world
world
world
world
hello
hello
hello
hello
hello

上例會先執行完 say("world") 後再執行 say("hello")

但有時個別方法的處理是沒有先後順序的,這時善用多執行緒就可以大大的提升效率。

多執行緒

在多執行緒下,最多可以同時執行與 CPU 數相等的 Goroutine。

// multi-thread.go
func main() {
    go say("world")
    say("hello")
}
world
hello
hello
world
world
hello
hello
world
world
hello

如此一來, say("world")會跑在另一個執行緒(Goroutine)上,使其並行執行。

CPU 數可以使用 runtime.NumCPU() 取得。

Goroutine 介紹

可以想成建立了一個 Goroutine 就是建立了一個新的 Thread。

go f(x, y, z)
  • go 開頭的函式叫用可以使 f 跑在另一個 Goroutine 上
  • f, x, y, z 取自目前的 goroutine
  • main 函式也是跑在 Goroutine 上
  • Main Goroutine 執行結束後, 其他的 Goroutine 會跟著強制關閉

等待

多執行緒下,經常需要處理的是執行緒之間的狀態管理,其中一個經常發生的事情是等待,例如A執行緒需要等B執行緒計算並取得資料後才能繼續往下執行,在這情況下等待就變得十分重要。

應該等待的時機

func main() {
    go say("world")
    go say("hello")
}

這個狀態下會有三個 Goroutine:

  • main
  • say("world")
  • say("hello")

這裡的問題發生在 main Goroutine 結束時,另外兩個 say Goroutine 會被強制關閉導致結果錯誤,這時就需要等待其他的 Goroutine 結束後 main Goroutine 才能結束。

接下來會介紹三種等待的方式,並且分析其利弊:

  • time.Sleep: 休眠指定時間
  • sync.WaitGroup: 等待直到指定數量的 Done() 叫用
  • Channel 阻塞: 使用 Channel 阻塞機制,使用接收時等待的特性避免執行緒繼續執行

time.Sleep

使 Goroutine 休眠,讓其他的 Goroutine 在 main 結束前有時間執行完成。

// sleep.go
func main() {
    go say("world")
    go say("hello")

    time.Sleep(5 * time.Second)
}

缺點:

  • 休息指定時間可能會比 Goroutine 需要執行的時間長或短,太長會耗費多餘的時間,太短會使其他 Goroutine 無法完成

sync.WaitGroup

// wait-group.go
func say(s string, wg *sync.WaitGroup) {
    defer wg.Done()

    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    wg := new(sync.WaitGroup)
    wg.Add(2)

    go say("world", wg)
    go say("hello", wg)

    wg.Wait()
}

  • 產生與想要等待的 Goroutine 同樣多的 WaitGroup Counter
  • WaitGroup 傳入 Goroutine 中,在執行完成後叫用 wg.Done() 將 Counter 減一
  • wg.Wait() 會等待直到 Counter 減為零為止

優點

  • 避免時間預估的錯誤

缺點

  • 需要手動配置對應的 Counter

Channel

最後介紹的是使用 Channel 等待, 原為 Goroutine 溝通時使用的,但因其阻塞的特性,使其可以當作等待 Goroutine 的方法。

// channel-wait.go
func say(s string, c chan string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
    c <- "FINISH"
}

func main() {
    ch := make(chan string)

    go say("world", ch)
    go say("hello", ch)

    <-ch
    <-ch
}

起了兩個 Goroutine(say("world", ch), say("hello", ch)) ,因此需要等待兩個 FINISH 推入 Channel 中才能結束 Main Goroutine。

優點

  • 避免時間預估的錯誤
  • 語法簡潔

Channel 阻塞的方法為 Go 語言中等待的主要方式。

多執行緒下的共享變數

在執行緒間使用同樣的變數時,最重要的是確保變數在當前的正確性,在沒有控制的情況下極有可能發生問題,下面有個例子:

// total-error.go
func main() {
    total := 0
    for i := 0; i < 1000; i++ {
        go func() {
            total++
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(total)
}

假設目前加到28,在多執行緒的情況下:

  • goroutine1 取值 28 做運算
  • goroutine2 有可能在 goroutine1total++ 前就取 total 的值,因此有可能取到 28
  • 這樣的情況下做兩次加法的結果會是 29 而非 30

在多個 goroutine 裡對同一個變數total做加法運算,在賦值時無法確保其為安全的而導致運算錯誤,此問題稱為 Race Condition

互斥鎖(sync.Mutex)

在這種狀況下,可以使用互斥鎖(sync.Mutex)來保證變數的安全:

// total-mutex.go
type SafeNumber struct {
    v   int
    mux sync.Mutex // 互斥鎖
}

func main() {
    total := SafeNumber{v: 0}
    for i := 0; i < 1000; i++ {
        go func() {
            total.mux.Lock()
            total.v++
            total.mux.Unlock()
        }()
    }
    time.Sleep(time.Second)
    total.mux.Lock()
    fmt.Println(total.v)
    total.mux.Unlock()
}

互斥鎖使用在資料結構(struct)中,用以確保結構中變數讀寫時的安全,它提供兩個方法:

  • Lock
  • Unlock

LockUnlock 中間,會使其他的 Goroutine 等待,確保此區塊中的變數安全。

藉由 Channel 保證變數的安全性

// total-channel.go
func main() {
    total := 0
    ch := make(chan int, 1)
    ch <- total
    for i := 0; i < 1000; i++ {
        go func() {
            ch <- <-ch + 1
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(<-ch)
}

  • goroutine1 拉出 total 後,Channel 中沒有資料了
  • 因為 Channel 中沒有資料,因此造成 goroutine2 等待
  • goroutine1 計算完成後,將 total 推入 Channel
  • goroutine2 等到 Channel 中有資料,拉出後結束等待,繼續做運算

因為 Channel 推入及拉出時等待的特性,被拉出來做計算的值會保證是安全的。

因為此範例一定要拉出 Channel 資料才能做運算,所以使用非立即阻塞的 Buffered Channel ,與 Unbuffered Channel 的差別等下會說明。

上述的三個例子在 main goroutine 中都使用 time.Sleep 避免程式提前結束。

Channel 介紹

上面藉由兩個在多執行緒中重要的議題:等待變數的共享,帶出 Channel 強大的處理能力,接著來深入瞭解一下 Channel。

Channel 可以想成一條管線,這條管線可以推入數值,並且也可以將數值拉取出來。

因為 Channel 會等待至另一端完成推入/拉出的動作後才會繼續往下處理,這樣的特性使其可以在 Goroutines 間同步的處理資料,而不用使用明確的 lock, unlock 等方法。

建立 Channel

ch := make(chan int) // 建立 int 型別的 Channel

推入/拉出 Channel 內的值,使用 <- 箭頭運算子:

  • Channel 在 <- 左邊:將箭頭右邊的數值推入 Channel 中
ch <- v    // Send v to channel ch.
v := <-ch  // Receive from ch, and assign value to v.

Channel 的阻塞

Goroutine 使用 Channel 時有兩種情況會造成阻塞:

  • 將資料推入 Channel,但其他 Goroutine 還未拉取資料時,將資料推入的 Goroutine 會被迫等待其他 Goroutine 拉取資料才能往下執行

  • 當 Channel 中沒有資料,但要從中拉取時,想要拉取資料的 Goroutine 會被迫等待其他 Goroutine 推入資料並自己完成拉取後才能往下執行

Goroutine 推資料入 Channel 時的等待情境

// channel-block-push.go
func main() {
    ch := make(chan string)

    go func() { // calculate goroutine
        fmt.Println("calculate goroutine starts calculating")
        time.Sleep(time.Second) // Heavy calculation
        fmt.Println("calculate goroutine ends calculating")

        ch <- "FINISH" // goroutine 執行會在此被迫等待

        fmt.Println("calculate goroutine finished")
    }()

    time.Sleep(2 * time.Second) // 使 main 比 goroutine 慢
    fmt.Println(<-ch)
    time.Sleep(time.Second)
    fmt.Println("main goroutine finished")
}
calculate goroutine starts calculating
calculate goroutine ends calculating
FINISH
calculate goroutine finished
main goroutine finished

此例使用 time.Sleep 強迫 main 執行慢於 calculate,現在來觀察輸出的結果:

  • calculate 會先執行並且計算完成
  • calculate 將 FINISH 訊號推入 Channel
  • 但由於目前 main 還未拉取 Channel 中的資料,所以 calculate 會被迫等待,因此 calculate 的最後一行 fmt.Println("main goroutine finished") 沒有馬上輸出在畫面上
  • main 拉取了 Channel 中的資料
  • calculate 執行fmt.Println("main goroutine finished") 並結束
  • main 執行完成

Goroutine 拉資料出 Channel 時的等待情境

// channel-block-pull.go
func main() {
    ch := make(chan string)

    go func() {
        fmt.Println("calculate goroutine starts calculating")
        time.Sleep(time.Second) // Heavy calculation
        fmt.Println("calculate goroutine ends calculating")

        ch <- "FINISH"

        fmt.Println("calculate goroutine finished")
    }()

    fmt.Println("main goroutine is waiting for channel to receive value")
    fmt.Println(<-ch) // goroutine 執行會在此被迫等待
    fmt.Println("main goroutine finished")
}
main goroutine is waiting for channel to receive value
calculate goroutine starts calculating
calculate goroutine ends calculating
calculate goroutine finished
FINISH
main goroutine finished
  • main 因拉取的時候 calculate 還沒將資料推入 Channel 中,因此 main 會被迫等待,因此 main 的最後一行 fmt.println 沒有馬上輸出在畫面上
  • calculate 執行並且計算完成
  • calculate 將 FINISH 推入 Channel
  • calculate 執行完成
  • main 拉取了 Channel 中的資料並且執行完成

Unbuffered Channel

前面一直提到的是 Unbuffered Channel,此種 Channel 只要

  • 推入一個資料會造成推入方的等待
  • 拉出時沒有資料會造成拉出方的等待

使用 Unbuffered Channel 的壞處是:如果推入方的執行一次的時間較拉取方短,會造成推入方被迫等待拉取方才能在做下一次的處理,這樣的等待是不必要並且需要被避免的。

為瞭解決推入方等待問題,可以使用另一種 Channel:Buffered Channel。

Buffered Channel

ch: make(chan int, 100)

Buffered Channel 的宣告會在第二個參數中定義 buffer 的長度,它只會在 Buffered 中資料填滿以後才會阻塞造成等待,以上例來說:第101個資料推入的時候,推入方的 Goroutine 才會等待。

下面的例子分別使用 Buffered Channel 跟 Unbuffered Channel 的差別:

// unbuffered-channel-error.go
func main() {
    ch := make(chan int)
    ch <- 1 // 等到天荒地老
    fmt.Println(<-ch)
}
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        /go/unbuffered-channel-error.go:9 +0x59
exit status 2

上例使用 Unbuffered Channel:

  • 只有一條 Goroutine:main
  • 推入 1 後因為還沒有其他 Goroutine 拉取 Channel 中的資料,所以進入阻塞狀態
  • 因為 main 已經在推入資料時阻塞,所以拉取的程式永遠不會被執行,造成死結

在相同的情況下,Buffered Channel 並不會被阻塞:

// buffered-channel.go
func main() {
    ch := make(chan int, 1)
    ch <- 1
    fmt.Println(<-ch)
}

原因是:

  • 推入 1 後 Channel 內的資料數為1並沒有超過 Buffer 的長度1,所以不會被阻塞
  • 因為沒有阻塞,所以下一行拉取的程式碼可以被執行,並且完成執行

Loop 中的 Channel

在迴圈中的 Channel 可以藉由第二個回傳值 ok 確認 Channel 是否被關閉,如果被關閉的話代表此 Channel 已經不再使用,可以結束巡覽。

// for-loop.go
func main() {
    c := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            c <- i
        }
        close(c) // 關閉 Channel
    }()
    for {
        v, ok := <-c
        if !ok { // 判斷 Channel 是否關閉
            break
        }
        fmt.Println(v)
    }
}
0
1
2
3
4
5
6
7
8
9

如果對 Closed Channel 推入資料的話會造成 Panic:

// closed-channel-panic.go
func main() {
    c := make(chan int)
    close(c)
    c <- 0 // Panic!!!
}
panic: send on closed channel

為了避免將資料推入已關閉的 Channel 中造成 Panic,Channel 的關閉應該由推入的 Goroutine 處理。

range 中的 Channel

range 是可以巡覽 Channel 的,終止條件為 Channel 的狀態為已關閉的(Closed):

// range.go
func main() {
    c := make(chan int, 10)
    go func() {
        for i := 0; i < 10; i++ {
            c <- i
        }
        close(c) // 關閉 Channel
    }()
    for i := range c { // 在 close 後跳出迴圈
        fmt.Println(i)
    }
}

使用 select 避免等待

在 Channel 推入/拉取時,會有一段等待的時間而造成 Goroutine 無法回應,如果此 Goroutine 是負責處理畫面的,使用者就會看到畫面 lag 的情況,這是我們不想見的情況。

例如之前提到的例子:

// block.go
func main() {
    ch := make(chan string)

    go func() {
        fmt.Println("calculate goroutine starts calculating")
        time.Sleep(time.Second) // Heavy calculation
        fmt.Println("calculate goroutine ends calculating")

        ch <- "FINISH"

        fmt.Println("calculate goroutine finished")
    }()

    fmt.Println("main goroutine is waiting for channel to receive value")
    fmt.Println(<-ch) // goroutine 執行會在此被迫等待
    fmt.Println("main goroutine finished")
}
main goroutine is waiting for channel to receive value # main goroutine 阻塞
calculate goroutine starts calculating
calculate goroutine ends calculating
calculate goroutine finished
FINISH # main goroutine 解除阻塞
main goroutine finished

main goroutine 要拉取 ch 的資料時,會被迫等待,這時會無法回饋目前的狀態給使用者,造成卡頓的清況。

這時可以使用 Go 提供的 select 語法,讓開發者可以很輕鬆的處理 Channel 的多種情況,包括阻塞時的處理。

// select.go
func main() {
    ch := make(chan string)

    go func() {
        fmt.Println("calculate goroutine starts calculating")
        time.Sleep(time.Second) // Heavy calculation
        fmt.Println("calculate goroutine ends calculating")

        ch <- "FINISH"
        time.Sleep(time.Second)
        fmt.Println("calculate goroutine finished")
    }()

    for {
        select {
        case <-ch: // Channel 中有資料執行此區域
            fmt.Println("main goroutine finished")
            return
        default: // Channel 阻塞的話執行此區域
            fmt.Println("WAITING...")
            time.Sleep(500 * time.Millisecond)
        }
    }
}
WAITING... # main goroutine 在阻塞時可以回應
calculate goroutine starts calculating
WAITING... # main goroutine 在阻塞時可以回應
WAITING... # main goroutine 在阻塞時可以回應
calculate goroutine ends calculating
main goroutine finished # main goroutine 解除阻塞並結束程式

將剛剛的例子改為 select 來處理,可以使 Channel 的推入/拉取不會阻塞:

  • 會在沒有阻塞的情況下才會執行對應的區塊
  • case <-ch:: 會等到沒有阻塞情況時(ch 內有資料)才會執行
  • default:: 在所有的 case 都阻塞的情況下執行

因為有 default 可以設置,當 Channel 阻塞時也可以藉由 default 輸出資訊讓使用者知道。

總結

一開始提到了單執行緒跟多執行緒的差別,接著帶出 Goroutine ,並介紹各種等待方式(time.Sleep, sync.WaitGroup 及 Channel)和執行緒間分享變數的問題(Race Condition)及解決方法(sync.Mutex 及 Channel),從而帶出 Channel 在執行緒中方便強大的能力。

再來講述 Channel 的使用方式,及其阻塞的時機(推入阻塞及拉取阻塞),接著說明 Unbuffered 及 Buffered Channel 的差別,並且說明可以藉由 Unbuffered Channel 降低效能上的損失。

Channel 傳回的第二個參數:ok,可以判斷此 Channel 是否已經關閉,並被 range 用在結束巡覽的判斷中。

最後說明瞭 select 可以 Channel 在阻塞時讓 Goroutine 保持非阻塞的狀態避免卡頓。

藉由 Goroutine 及 Channel 簡單的語法但是強大的能力使工程師開發多工程式的時候可以寫出優雅又易於維護的代碼,是 Go 語言的優勢之一。

參考資料

出處

https://peterhpchen.github.io/2020/03/08/goroutine-and-channel.html

WebSocket

package main

import (
	"log"
    "fmt"

	"github.com/gorilla/websocket"
)

// https://github.com/binance-exchange/go-binance/blob/master/service_websocket.go
func main() {
	c, _, err := websocket.DefaultDialer.Dial("wss://stream.binance.com:9443/ws/btsusdt@depth20@100ms", nil)
	if err != nil {
		log.Fatal("dial:", err)
	}
	defer c.Close()

	// 啟動一個協程,讀取從服務端發送過來的數據
	go func() {
		for {
			_, message, _ := c.ReadMessage()
			fmt.Println(string(message))
		}
	}()

	// 阻塞主線程
	down := make(chan byte)
	for {
		<-down
	}
}
func main() {

    // 定義客戶端的地址
    u := url.URL{Scheme: "ws", Host: "locaalhost:999", Path: "/connect"}

    // 與客戶端建立連接
    c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
    if err != nil {
        log.Fatal("dial:", err)
    }
    defer c.Close()

    // 啟動一個協程,讀取從服務端發送過來的數據
    go func() {
        for {
            _, message, _ := c.ReadMessage()
            fmt.Println(string(message))
        }
    }()


    // 阻塞主線程
    down := make(chan byte)
    for {
        <-down
    }
}
package main

import (
    "flag"
    "fmt"
    "github.com/gorilla/websocket"
    "log"
    "net/url"
)

var addr = flag.String("addr", "localhost:9999", "proxy server addr")

func main() {

    u := url.URL{Scheme: "ws", Host: *addr, Path: "/connect"}

    c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
    if err != nil {
        log.Fatal("dial:", err)
    }
    defer c.Close()

    down := make(chan byte)
    go func() {
        for {
            _, message, _ := c.ReadMessage()
            fmt.Println("服務端發送:" + string(message))
        }
    }()

    go func() {
        for {
            var input string
            fmt.Scanln(&input)
            c.WriteMessage(websocket.TextMessage, []byte(input))
        }
    }()

    for {
        <-down
    }
}

Returning Pointer from a Function in Go

Pointers in Go programming language or Golang is a variable which is used to store the memory address of another variable. We can pass pointers to the function as well as return pointer from a function in Golang. In C/C++, it is not recommended to return the address of a local variable outside the function as it goes out of scope after function returns. So to execute the concept of returning a pointer from function in C/C++ you must define the local variable as a static variable.

Example: In the below program, the line of code(int lv = n1 * n1;) will give warning as it is local to the function. To avoid warnings make it static.

// C++ program to return the
// pointer from a function
#include <iostream>
using namespace std;
  
// taking a function having
// pointer as return type
int* rpf(int);
  
int main()
{
  
    int n = 745;
  
    // displaying the value of n
    cout << n << endl;
  
    // calling the function
    cout << *rpf(n) << endl;
}
  
// defining function
int* rpf(int n1)
{
  
    // taking a local variable
    // inside the function
    int lv = n1 * n1;
  
    // remove comment or make the above
    // declaration as static which
    // result into successful
    // compilation
    // static int lv = n1 * n1;
  
    // this will give warning as we
    // are returning the address of
    // the local variable
    return &lv;
}

Output:

745

The main reason behind this scenario is that compiler always make a stack for a function call. As soon as the function exits the function stack also get removed which causes the local variables of functions goes out of scope. Making it static will resolve the problem. As static variables have a property of preserving their value even after they are out of their scope.

But the Go compiler is very Intelligent!. It will not allocate the memory on the stack to the local variable of the function. It will allocate this variable on the heap. In the below program, variable lv will have the memory allocated on the heap as Go compiler will perform escape analysis to escape the variable from the local scope.

Example:

 Go program to return the
// pointer from the function
package main
  
import "fmt"
  
// main function
func main() {
  
    // calling the function
    n := rpf()
  
    // displaying the value
    fmt.Println("Value of n is: ", *n)
  
}
  
// defining function having integer
// pointer as return type
func rpf() *int {
  
    // taking a local variable
    // inside the function
    // using short declaration
    // operator
    lv := 100
  
    // returning the address of lv
    return &lv
}

Output:

Value of n is:  100

Note: Golang doesn’t provide any support for the pointer arithmetic like C/C++. If you will perform then the compiler will throw an error as invalid operation.

Golang 記憶體管理 GC 全面解析

出處 : https://alanzhan.dev/post/2022-02-13-golang-memory-management/

記憶體管理的爭論

關於記憶體管理,往往都會討論到一個持續很久的爭論,就是記憶體到底要給誰管?給機器管?還是給人管?不管是機器管或者是人管,大家的初衷都是一致的,認為記憶體管理是非常重要的,但大家的意見還是分歧了:

  • c / c++ :認為記憶體管理如此重要,所以我希望把記憶體管理的自由交付給工程師,因為這些人我相信他的技能非常的強,他知道甚麼時候該申請記憶體,甚麼時候該釋放記憶體。
  • Java / .Net(C#) / golang / etc :它們觀點卻站在反面,目的雖然一樣,認為記憶體管理如此重要,但是我們不能相信人,我希望通過自動化的方式管理記憶體。

我們可以思考一下這兩者的差異,當然 c / c++ 記憶體使用與釋放的效率非常的高,因為工程師知道我的記憶體甚麼時候不用,我直接把他 free 掉,但是人總是會犯錯,如果有人忘記釋放記憶體,那麼就會導致記憶體洩漏,最終導致程式崩潰。

在越是年輕的語言,在追求的反而是開發的效率,希望可以通過一種自動化的方式管理記憶體,減少人為的錯誤,使得開發的效率變高,所以記憶體的管理反而變得十分的重要,那麼記憶體管理會面臨哪些挑戰呢?

Heap 記憶體管理的挑戰

  • 記憶體分配需要系統調用,在頻繁記憶體分配的時候,系統性能較低。
  • 多線程共享相同的記憶體空間時,同時申請記憶體,需要加鎖,否則會產生同一塊記憶體被多個線程訪問的狀況。
  • 記憶體碎片化的問題,經過不斷的記憶體分配與回收,記憶體碎片會比較嚴重,記憶體的使用效率會降低。

所以這是 c / c++ 這些比較傳統的語言,假如自己去申請 heap 記憶體,如果不做處理,可能會引發的問題。

Heap 記憶體管理

那麼現在的語言要怎麼解決這種問題呢?

假設 heap 是目前的所擁有的 heap 記憶體,針對這個 heap 的管理主要會有三個角色跟次要輔助用的 Header:

  • Allocator : 記憶體的分配器,主要動態處理記憶體的分配請求,程式啟動時 Allocator 會在初始化的時候,預先向操作系統申請記憶體,接下來可能會先記憶體做一定的格式化。
  • Mutator : Mutator 可以理解為我們的程式,Mutator 只要負責跟 Allocator 申請記憶體就好,他不需要顯式的去釋放(回收)記憶體。
  • Collector : 垃圾回收器,回收記憶體空間,他會去掃描整的 heap 記憶體,哪些是活躍物件,那些是非活耀物件,當發現非活耀,就會回收記憶體。
  • Object Header : 當記憶體分配出去時,同時會對這塊記憶體做標記,用來標記物件的, Collector 和 Allocator 會來同步物件 Metadata。

大家可以根據上面的敘述稍微順一下,那麼我們就來看看 golang 怎麼處理的:

  • 初始化連續記憶體作為 heap 。
  • 有記憶體申請的時候,Allocator 從 heap 記憶體的未分配區塊切割小記憶體塊。
  • 用鏈表將以分配的記憶體連接起來。
  • 需要描述每個記憶體塊的 metadata ,大小、是否使用、下一塊記憶體位置等。

TCMalloc

golang 這門語言的記憶體管理是基於 TCMalloc 基礎上進行設計的,所以在認識 golang 記憶體管理之前,先梳理一下 TCMalloc (Thread Cache Malloc) 的原理。

我們先回想剛剛所講述的記憶體管理會面臨哪些挑戰,Heap 記憶體管理的挑戰

  • 記憶體分配需要系統調用,在頻繁記憶體分配的時候,系統性能較低。
    • 所以 TCMalloc 會先去申請記憶體並且預分配記憶體。
  • 多線程共享相同的記憶體空間時,同時申請記憶體,需要加鎖,否則會產生同一塊記憶體被多個線程訪問的狀況。
    • 可以看到 ThreadCache 那個區塊,他為了每一個 thread 維護了一塊 ThreadCache ,而且每一個都是線程獨立的記憶體空間,也就是說,當 application 要申請記憶體的時候,他會優先向 ThreadCache 申請,也因為 ThreadCache 各自維護了各自的記憶體,所以 application 要申請記憶體的時候,不需要加鎖去申請。
    • 如果 ThreadCache 把記憶體用盡了怎魔辦?他會去向 CentralCache 申請記憶體,但是這個時候需要加鎖,但是聰明的你已經發現,加鎖的可能性已經變低了。
    • 如果 CentralCache 也沒有空間了,他就會向 PageHeap 申請空間。
    • 如果 PageHeap 也沒有空間了,他就會向 VirtualMemory 申請更多記憶體。

所以 TCMalloc 解決了記憶體管理會面臨的挑戰以及記憶體的逐級申請機制,那我們在想一下,假設 application 都向 ThreadCache 申請記憶體,而且都不管物件大小,拿來就用,那這不就意味著記憶體管理是很混亂的嗎?

TCMalloc 對於這種混亂的場景又做了增強,他把記憶體分為不同等級 (Size Class),首先他申請記憶體的動作還按照一個頁一個頁去申請的,但是這個頁的大小是 8K,他會把申請的記憶體,按照不同的 Size Class (每個 Size Class 都會對應一個大小,譬如 8 byte、16 byte) 劃分,總共劃分了 128 種,而相同的大小 Size Class 會組成 Span list,假如 application 去申請一個 byte ,TCMalloc 就會從 Size Class 0 分配記憶體給 application,這是小物件的狀態,但是如果申請大物件的時候,會跳過 ThreadCache 與 CentralCache 去跟 PageHeap 申請記憶體,這就是 TCMalloc 的實現原理。

  • page : 記憶體頁,一塊 8K 大小。 golang 與操作系統之間的記憶體申請與釋放,都是以 page 為單位。
  • span : 記憶塊,一個或多個連續的 page 組成一個 span。
  • sizeclass : 空間規格,每個 span 都會帶有一個 sizeclass,標記 span 中的 page 應該如何使用。
  • object : 物件,用來存儲一個變數數據的記憶體空間,一個 span 在初始化的時候,就會被切割成一堆等大的物件。假設 object 的大小為 16B , span 大小為 8K ,那麼 span 中的 page 就會被初始化為 8K / 16B = 512 個 object,當 application 來申請的時候,就是分配一個 object 出去。
  • 物件大小定義
    • 小物件 : 0 ~ 256KB
    • 中物件 : 256KB ~ 1MB
    • 大物件 : > 1MB
  • 小物件分配流程 : ThreadCache -> CentralCache -> HeapPage,大部分時候, ThreadCache 的緩存都是足夠的,不需要去訪問 CentralCache 和 HeapPage,無須加鎖,所以分配效率是很高的。
  • 中物件分配流程 : 直接在 PageHeap 中挑選適當大小的即可,128 Page 的 Span 保存的就是最大的 1MB。
  • 大物件分配流程 : 從 large span set 選擇合適數量頁面組成 span ,用來存儲數據。

Golang 記憶體分配

golang 記憶體分配基本上與 TCMalloc 一致,它是在 TCMalloc 在原型上修改與增強,看下面這張圖會看起來很像 TCMalloc,但是還是有一些差異的。

  • 在 mcache 內一個 span class 對應兩個 span class ,一個是用來存指針的,一個是用來存直接引用的,存直接引用的 span 無須 GC。
  • 當 mcache 記憶體不夠的時候,會向 mcentral 申請,會優先去 nonempty 的鏈找,因為 nonempty 保存的是這邊有可用的 page,如果還是找不到,就會去 mheap 申請。
    • 補充 : mcache 從 mcentral 獲取和歸還 span 。
      • 獲取時,上鎖,從 nonempty 鏈表找到一個可用的 span,並且將其從 nonempty 鏈表刪除,將取出的 span 加入到 empty 鏈,將 span 返回給工作線程,解鎖。
      • 歸還時,上鎖,將 span 從 empty 鏈中刪除,將 span 加入到 nonempty 鏈,解鎖。
  • 在 meap 內依照 Span Class 維護了一個 Binary Sort Tree ,但是他維護了兩棵樹。
    • free : free 中保存的 span 是空閒的,非垃圾回收的 span。
    • scav : scav 中保存的是空閒的,並且已經垃圾回收的 span。
    • 如果是垃圾回收導致 span 的是放,sapn 會被加入到 scav 中,否則會被加入到 free ,比如剛從 Virtual Memory 申請的記憶體。

  • mcache : 小物件的記憶體分配。
    • size class 總共有 67 個,而 class = 0 是特殊的 span,用於大於 32 kb 的物件,每個 class 兩個 span 。
    • span 大小是按照 8KB ,按照 span class 大小切分。
  • mcentral
    • 當 mcache 的 span 內所有記憶體塊都被佔用的時候, mcache 會向 mcentral 申請一個 span, mache 拿到 span 後繼續分配物件。
    • 當 mcentral 向 mcache 提供 span 時,如果沒有符合的 span , mcentral 會向 mheap 申請 span。
  • mheap
    • 當 mheap 沒有足夠的記憶體時,mheap 會向 OS 申請記憶體。
    • mheap 維護 span 不再是鏈表了,而是 Binary Sort Tree 。
    • heap 會進行 span 的維護,它包含了地址 mapping 和 span 是否包含指針 metadata,目的是為了更高效的分配、回收與再利用。

記憶體回收

常見記憶體回收策略

引用計數

  • 常見語言 : Python 、 PHP 、 Swift
  • 特性 : 對每一個物件維護一個引用計數,當引用該物件的物件被銷毀時,引用計數就減 1 ,當引用計數為 0 時,就回收該物件。
  • 優點 : 物件可以很快的被回收,不會出現記憶體耗盡或者達到某個閥值才回收。
  • 缺點 : 不能很好的處理循環引用,而且維護引用計數,也有一定的代價。

標記清除

  • 常見語言 : golang
  • 特性 : 從根變數開始遍歷檢查所有引用物件,引用物件被標計為「被引用」,沒有被標記的就進行回收。
  • 優點 : 解決引用技術的缺點。
  • 缺點 : 需要 STW (Stop the world),即要暫停程式運行。

分代收集

  • 常見語言 : Java 、 .Net(C#) 、 Nodejs (Javascript)
  • 按照生命週期進行劃分不同代空間,生命週期較長的放入老生代,短的放入新生代,新生代的回收頻率會高於老生代的頻率,通常會被分為三代。
    • Young : 或者被稱為 eden ,存放新創的物件,物件生命週期非常的短,幾乎用完就可以被回收。
    • Tenured :或者被稱為 old , 在 Young 區多次回收後存活下來的物件,將被移轉到 Tenured 區。
    • Perm : 永久代,主要存加載類的資訊,生命週期較長,幾乎不會被回收。
  • 優點 : 大部分的物件都是朝生夕死的,所以可以更高效的清除用完即丟的物件。
  • 缺點 : 演算法較為複雜,執行的步驟較多。

Golang GC 工作流程

golang GC 的大部分處理是和用戶程式碼並行的,大致上分為四個步驟,基本上就是標記與清除 Mark 與 Sweep。

  • Mark
    • Mark Prepare : 初始化 GC 任務,包括開啟屏障 (WB : write barrier) 和輔助 GC (mutator assis),和統計 root 物件的任務數量等,這時候需要 STW (stop the world)。
    • GC Drains : 掃描所有的 root 物件,包括全局指針和 goroutine (G) stack 上的指針 (掃描對應的 G 時,需要停止該 G),將其加入標計對列 (灰色對列),並循環處理灰色對列的物件,直到灰色對列為空,這個過程是背景並行處理的。
  • Mark Termination : 完成標計工作,重新掃描全局指針和 stack。因為 Mark 和用戶的程式是併行的,所以在 Mark 過程中也有可能會有新的物件和指針賦值,這個時候需要通過屏障記錄下來,然後在 rescan 檢查一下,這個過程也是會 STW 的。
  • Sweep : 按照標記結果回收所有白色對象,這個過程是背景平行處理的。
  • Sweep Termination : 對未清理的 span 進行清理,只有上一輪的 GC 清理完畢,才會開始新一輪的 GC 。

三色標計法

  • GC 開始時,默認所有的 object 都是垃圾,所以都是白色。
  • 從 root 區開始遍歷查找,被找到的物件會被標計為灰色。
  • 從所有灰色的 物件,將他們內部引用的變數標記為灰色,自己則標計為黑色。
  • 循環上面的步驟,直到沒有灰色的物件,只剩下黑白兩種,白色的都是垃圾。
  • 對於黑色的物件,如果在標記期間發生寫操作,寫屏障會在真正賦值前將物件標計為灰色。
  • 標記過程中,mallocgc 新分配的物件,會先被標計為黑色再返回。

Golang 垃圾回收觸發機制

  • 記憶體分配量達到閥值觸發 GC
    • 每次記憶體分配都會檢查當前記憶體分配量,是否已經達到閥值,如果達到閥值則會立即啟動 GC。
      • 閥值 = 上次 GC 記憶體分配量 * 記憶體增長量。
      • 記憶體增長量,由環境變數 GOGC 控制,默認為 100,即每當記憶體擴大一倍的時候,啟動 GC。
  • 定期觸發 GC
    • 默認情況下,每兩分鐘觸發一次 gc,這個間隔在 src/rumtime/proc.go:forcegcperiod 變數中被宣告。
  • 手動觸發
    • 程式代碼中,也可以使用 runtime.GC() 來手動觸發GC。這個主要用於測試 GC 性能和統計。

總結

上一篇讀了那麼硬的知識後,今天來挑戰記憶體管理的歷史到 golang 的記憶體管理相關知識,讀完之後覺得有點痛苦,但是這將會化身成為我們成長的一大養分不是嗎?看完之後你的感想為何呢?

歡迎到我的 Facebook Alan 的筆記本 留言,順手給我個讚吧!你的讚將成為我持續更新的動力,感謝你的閱讀,讓我們一起學習成為更好的自己。

參考

Golang Goroutine 與 GMP 原理全面分析

最近在研讀 Kubernetes ,所以得好好地跟 golang 這個語言當朋友,看著看著看到了 goroutine ,但是始終不解 goroutine 是哪來幹嘛、為何而生的?所以我們在開始深入認識 goroutine 之前,我們可能要先來認識一下歷史,這樣我們才能更全面的認識 goroutine 的原理與設計思想。

Golang 調度器的由來

單進程時代

我們都知道軟體是跑在操作系統之上的,真正來計算的人是 CPU,早期的操作系統每個程序就是一個進程,直到一個程序運行完畢之後,才能運行下一個進程。

假設有三個進程,分別為 A 、 B 與 C ,那麼在 CPU 上的調度就是依照執行順序執行。

Example: A -> B -> C

但是在這樣的單進程操作系統時代,會面臨以下的問題:

  1. 每次只能執行一個進程,計算機只能一個任務一個任務的執行。
  2. 若進程發生了 IO 操作堵塞時,容易造成 CPU 資源的浪費。

於是就誕生了多進程 / 多線程 的操作系統。

多進程 / 多線程時代

在多進程 / 多線程德操作系統中,就解決掉了阻塞的問題,因為一個進程阻塞 CPU 就可以立刻切換到其他進程中去執行,而且調度 CPU 的算法可以保證運行的進程,都可以分配到 CPU 的運行分片,從宏觀的角度來看,似乎多個進程是同時在運行的,相信會有同學不清楚 CPU 的調度原理不清楚的話,可以查看一下 CPU 調度原理

多個進程在分配 CPU 的運行時間片的時候,一切看起來沒問題的,但是工程師們又發現了新的問題,進程在創建、切換、銷毀,都會佔用很長得時間,CPU 的利用率雖然起來了,但是進程過多時,CPU 會有很大一部分的時間都會被用來運行切換進程。

那麼進程在切換的時候,會造成那些開銷呢?

進程切換開銷

  • 直接開銷
    • 切換頁表全域性目錄 (PGD)
    • 切換 Kernel 堆疊
    • 切換硬體上下文 (進程恢復之前,必須裝入戰存器的資料,統稱為硬體上下文)
    • 重新整理 TLB
    • 系統調度器的代碼執行
  • 間接開銷
    • CPU 緩存失效導致進程需要用到內存直接訪問的 IO 操作變多

所以我們該如何才能提高 CPU 的利用率呢?

協程來提高 CPU 利用率

聰明的工程師們就發現,其實線程分為內核態線程 (Kernel Thread) 與用戶態線程 (User Thread),而一個「用戶態線程」必須綁定一個「內核態線程」,但是 CPU 不會知道有「用戶態線程」的存在,他只知道他運行的是一個「內核態線程」 (Linux PCB 進程控制塊)。

那麼我們能不能在用戶態創建維護一個輕量級的協程 (co-routine),讓多個輕量級的線程綁定到同一個內核態線程上?如果一個內核態線程分到一個運行的時間片之後,那我是不是能在有效的時間內,把用戶態堆積的所有協程都執行完成呢?然後再把 CPU 交出去,那這樣是不是整個執行效率會高很多呢?

所以這就是 Go 語言線程調度遵循的一些原則,那我們來看看 Goroutine。

Goroutine

Goroutine 就是 Go 語言的協程概念,Go 語言基於 GMP 模型實現用戶態線程

  • Goroutine : 表示 goroutine ,每個 goroutine 都有自已的 stack 空間、定時器,初始化的 stack 大小在 2k 左右,空間會隨著需求增長。
  • Machine : 抽象化代表內核線程,紀錄內核線程 stack 信息,當 goroutine 調度到線程時候,使用該 goroutine 自己的 stack 信息。
  • Process : 表示調度器,負責調度 goroutine ,維護一個本地 goroutine 對列,並且把對列跟 M 綁定,讓 M 從 P 上獲得 goroutine 並執行,同時還負責部分記憶體管理。

MPG 的對應關係

  • KSE: Kernel Scheduling Entity
  • M 我們可以理解跟 Kernel Task 一對一對應
  • 一個 P 上面可以有多個 G,P 會去識當前狀態來決定要跟哪個 M 來綁定,比如說一個 M 已經陷入到內核態,而 P 就有可能換主,去找其他 M 執行。

GMP 模型細節

  • LRQ: local run queue
  • GRQ: global run queue
  • sudog: 阻塞 queue
  • gFree: 全局自由 G 列表
  • pidle: 全局空閒 P 列表

以下真的上圖的一些細節展開贅述:

  • 假設 go 語言在主程序,起了多個 goroutine ,那麼在啟動的過程中,會有一個參數可以設定, go 可以運行多少個併發的現程,一般而言,會看你的節點上有多少個 CPU 併發數就是多少,所以在初始化的過程中,就會按照你的設定數量去初始化 P 。
  • 當 go 語言開始執行了,那其實 go 語言的 main 方法,本身也是一個 goroutine ,所以他就會被落到一個 P 上,那麼這個 main 方法又起了很多個 goroutine ,那麼他就會在當前的 P 上掛載多個 G ,所以在這一刻, P 上會有一堆排隊的 G ,這時候,還沒充分的利用多核心的優勢,因為 G 都掛載在同一個 P 上,但是其他的 P 不可能空手啥事都不幹,這樣未免也太浪費 CPU 了吧!
  • 如果 P 已經空手了,那麼他就會去看 GRQ ,如果還是沒有,他就會去看看其他的 P 是不是有 G 可以執行,假設他發現第一個 P 有正在對列的 G ,那麼他就會拿取一半的 G 過來運行,但是所有的 P 都有這個機制,所以很快的堆積的 G 就被消化完畢。
  • 但是會不會有一種狀況發生,我創建了一堆 G ,超出了 LRQ 的長度 (默認 256),那麼這個 G 就會放到 GRQ 內。
  • 如果 M 陷入內核態了,那麼 P 就會跟 M 斷開綁定關係, P 就會取找說 哪個 M 是空閒的,並且跟他綁定在一起。
  • 如果一個 G 產生了阻塞,那處於 wait 狀態的 G ,就會被丟到 sudog 阻塞對列裡,他不跟任何的 P 產生綁定關係。
  • 如果 G 已經完成運行後,他會把自己放到 gFree 去,這樣就可以重複使用 G ,減少開銷。

P 的狀態

  • _Pidle: 處理器沒有運行用戶代碼或者調度器,被空閒對列或者改變其狀態的結構持有,運行對列為空。
  • _Prunning: 被線程 M 持有,並且正在執行用戶代碼或者調度器。
  • _Psyscall: 沒有執行用戶代碼,當前線程陷入系統調用。
  • _Pgcstop: 被線程 M 持有,當前處理器於垃圾回收被停止。
  • _Pdead: 當前處理器已經不被使用。

G 的狀態

  • _Gidle: 剛剛被分配,並且還沒被初始化,值為 0 ,為創建 goroutine 後的默認值。
  • _Grunnable: 沒有執行代碼,沒有 stack 的所有權,存儲在運行對列中,可能在某個 P 的本地對列或者全局對列中。
  • _Grunning: 正在執行代碼的 goroutine,擁有 stack 的所有權。
  • _Gsyscall: 正在執行系統調用,擁有 stack 的所有權,與 P 脫離,但是與某個 M 綁定,會在調用結束後,被分配到運行對列。
  • _Gwaiting: 被阻塞的 goroutine,阻塞在某個 channel 的發送或者接收對列。
  • _Gdead: 當前 goroutine 未被使用,沒有執行代碼,可能有分配的 stack ,分佈在空閒列表,可能是一個剛初始化 goroutine ,也可能是執行 goexit 退出的 goroutine。
  • _Gcopystac: stack 正在被拷貝,沒有執行代碼,不在運行對列上,執行權在。
  • _Gscan: GC 正在掃描 stack 空間,沒有執行代碼,可以與其他狀態同時存在。

調度器行為

  • 為了保證公平,當 GRQ 中有待執行的 G 時候,通過 schedtick 保證有一定的機率 (1/61),會從 GRQ 中查找 G。
  • 從 P 的 LRQ 中查找待處理的 G。
  • 如果前面兩種都沒找到 G,會通過 runtime.findrunnable 進行阻塞查找 G。
    • 從 LRQ 、 GRQ 中查找。
    • 從網路輪詢器中查找是否有 G 等待運行。
    • 通過 runtime.runqsteal 嘗試從其他隨機的 P 中竊取一半的 G。

總結

好久沒看那麼硬的知識了,但我總覺得我今天會消化不良,我需要反反覆覆在看個好幾次,看完之後你的心得如何呢?

歡迎到我的 Facebook Alan 的筆記本 留言,順手給我個讚吧!你的讚將成為我持續更新的動力,感謝你的閱讀,讓我們一起學習成為更好的自己。

參考

GoLang - 物件導向

在這幾篇,會以 Go 語言的入門基礎進行逐步說明,本篇針對物件導向進行說明

在 Go 語言沒有像其他語言一樣有明確定義物件導向(Class, object, instance…) 封裝層,並且沒有 this, self 這種可以代表物件本身的屬性,以及沒有靜態屬性 (那麼本篇結束?)

當然,答案其實是不盡然,在 Go 雖然沒有其他語言有明確定義物件導向,但其實一樣也可實作出物件導向結構。

由於在實作方式會和其他語言有所不同,這部分會讓多數人搞混,就連官方看待 “Go是否為物件導向語言?” 問題,他們回答的是 “yes and no”,以含糊的方式回答。因此,當你進入 Go 的領域時,請放下過去 OO 包袱,重新在這裡學習 Go 語言的物件導向結構。

struct:

在開始說明 struct 在實作物件導向的用法前,先說明他的基本結構,基本上在宣告一個 struct 時,可以同時宣告他的屬性,例如:

struct 命名,若字首大寫則為 public 權限,如果小寫則只在自己的 package 內可以訪問。

type User struct{
	name string
	age int
	phone int
}

例如:

type User struct {
  name  string
  age   int
  phone string
}

func main() {

  var user1 User
  user1.name = "Adam"
  user1.age = 10
  user1.phone = "0912345678"

  fmt.Println(user1)
}
//output {Adam 10 0912345678}

另外,也可直接宣告 struct 預設值

type User struct {
  name  string
  age   int
  phone string
}

func main() {

  user1 := User{"Adam", 10, "0912345678"}

  fmt.Println(user1)
}
//output {Adam 10 0912345678}

這裡不詳細說明 struct 的各種用法,後續再針對這部份進行介紹,接著來說明如何在 method 如何來結合 struct與func做出物件導向。

method:

struct 的結構可以在 Go 語言中實作 Class 以及可定義屬性,而 函數 則可實作方法。並且 func 會各自獨立存在。

而 method 可以將 struct 與 func 建立關聯,他的基本結構為:

func (receiverParam ReceiverType) funcName(param paramType) (resultsType) {

}

在下方示範一個例子, user 表示為一般用戶,member 表示為付費會員,在這裡透過 methods 各別定義出兩者的 struct與 func,

func 可以看到,帶入的參數會指定是什麼 struct。

在這裡例子,兩個 methods 名稱都一樣 (都叫 data()) ,但是會依照收的 struct 不同來做區分:

type User struct {
  name  string
  age   int
  phone string
}

func (tg User) data() string {
  return "this is user:" + tg.name
}

type Member struct {
  name    string
  balance int
  age     int
  phone   string
}

func (tg Member) data() string {
  return "this is member:" + tg.name
}

func main() {

  user1 := User{"Adam", 10, "0912345678"}
  member1 := Member{"Brown", 11, 233, "0912345679"}

  fmt.Println(user1.data()) //output this is user:Adam
  fmt.Println(member1.data()) //output this is member:Brown

}

當然,這裡透過一個簡單的例子說明,希望對於學習 Go 語言的物件導向應用能有所幫助。在 Go 語言可以做出非常簡單優雅的物件導向架構。更進階的,可以透過 struct 匿名欄位來做出更多元的應用。最後,雖然沒有像其他程式語言,預設就攜帶著豐富的物件導向結構,但是在實際開發需求中,還是可以透過這樣的功能堆疊,進行開發。

建立類別 (Class) 和物件 (Object)

傳統的程序式程式設計 (procedural programming) 或是指令式程式設計 (imperative programming) 學到函式大概就算學完基本概念。

不過,近年來,物件導向程式設計 (object-oriented programming) 是程式設計主流的模式 (paradigm),即使 C 這種非物件導向的語言,我們也會用結構和函式模擬物件的特性。本文將介紹如何在 Go 撰寫物件導向程式。

五分鐘的物件導向概論

由於物件導向是程式設計主流的模式 (paradigm),很多語言都直接在語法機制中支援物件導向,然而,每個語言支援的物件導向特性略有不同,像 C++ 的物件系統相當完整,而 Perl 的原生物件系統則相對原始。物件導向在理論上是和語言無關的,但在實務上卻受到不同語言特性 (features) 的影響。學習物件導向時,除了學習在某個特定語言下的實作方式外,更應該學習其抽象層次的思維,有時候,暫時放下實作細節,從更高的視角看物件及物件間訊息的流動,對於學習物件導向有相當的幫助。

物件導向是一種將程式碼以更高的層次組織起來的方法。大部分的物件導向以類別 (class) 為基礎,透過類別可產生實際的物件 (object) 或實體 (instance) ,類別和物件就像是餅乾模子和餅乾的關係,透過同一個模子可以產生很多片餅乾。物件擁有屬性 (field) 和方法 (method),屬性是其內在狀態,而方法是其外在行為。透過物件,狀態和方法是連動的,比起傳統的程序式程式設計,更容易組織程式碼。

許多物件導向語言支援封裝 (encapsulation),透過封裝,程式設計者可以決定物件的那些部分要對外公開,那些部分僅由內部使用,封裝不僅限於靜態的資料,決定物件應該對外公開的行為也是封裝。當多個物件間互動時,封裝可使得程式碼容易維護,反之,過度暴露物件的內在屬性和細部行為會使得程式碼相互糾結,難以除錯。

物件間可以透過組合 (composition) 再利用程式碼。物件的屬性不一定要是基本型別,也可以是其他物件。組合是透過有… (has-a) 關係建立物件間的關連。例如,汽車物件有引擎物件,而引擎物件本身又有許多的狀態和行為。繼承 (inheritance) 是另一個再利用程式碼的方式,透過繼承,子類別 (child class) 可以再利用父類別 (parent class) 的狀態和行為。繼承是透過是… (is-a) 關係建立物件間的關連。例如,研究生物件是學生物件的特例。然而,過度濫用繼承,容易使程式碼間高度相依,造成程式難以維護。可參考組合勝過繼承 (composition over inheritance) 這個指導原則來設計自己的專案。

透過多型 (polymorphism) 使用物件,不需要在意物件的實作,只需依照其公開介面使用即可。例如,我們想要開車,不論駕駛 Honda 汽車或是 Ford 汽車,由於汽車的儀錶板都大同小異,都可以執行開車這項行為,而不需在意不同廠牌的汽車的內部差異。多型有許多種形式,如:

  • 特定多態 (ad hoc polymorphism):
    • 函數重載 (functional overloading):同名而不同參數型別的方法 (method)
    • 運算子重載 (operator overloading) : 對不同型別的物件使用相同運算子 (operator)
  • 泛型 (generics):對不同型別使用相同實作
  • 子類型 (Subtyping):不同子類別共享相同的公開介面,不同語言有不同的繼承機制

以物件導向實作程式,需要從宏觀的角度來思考,不僅要設計單一物件的公開行為,還有物件間如何互動,以達到良好且易於維護的程式碼結構。除了閱讀本教程或其他程式設計的書籍以學習如何實作物件外,可閱讀關於 物件導向分析及設計 (object-oriented analysis and design) 或是設計模式 (design pattern) 的書籍,以增進對物件導向的瞭解。

[Update on 2018/05/20] 嚴格來說,Go 只能撰寫基於物件的程式 (object-based programming),無法撰寫物件導向程式 (object-oriented programming),因為 Go 僅支援一部分的物件導向特性,像是 Go 不支援繼承。

由於 Go 的設計思維,以 Go 實作基於物件的程式時,會和 Java 或 Python 等相對傳統的物件系統略有不同,本文會在相關處提及相同及相異處,供讀者參考。

建立物件 (Object)

以下範例程式碼建立簡單的 Point 類別和物件:

package main                                  /*  1 */

import (                                      /*  2 */
        "log"                                 /*  3 */
)                                             /*  4 */

// `X` and `Y` are public fields.             /*  5 */
type Point struct {                           /*  6 */
        X float64                             /*  7 */
        Y float64                             /*  8 */
}                                             /*  9 */

// Use an ordinary function as constructor    /* 10 */
func NewPoint(x float64, y float64) *Point {  /* 11 */
        p := new(Point)                       /* 12 */

        p.X = x                               /* 13 */
        p.Y = y                               /* 14 */

        return p                              /* 15 */
}                                             /* 16 */

func main() {                                 /* 17 */
        p := NewPoint(3, 4)                   /* 18 */

        if !(p.X == 3.0) {                    /* 19 */
                log.Fatal("Wrong value")      /* 20 */
        }                                     /* 21 */

        if !(p.Y == 4.0) {                    /* 22 */
                log.Fatal("Wrong value")      /* 23 */
        }                                     /* 24 */
}                                             /* 25 */

第 6 行至第 9 行的部分是形態宣告。Golang 沿用結構體為類別的型態,而沒有用新的保留字。

第 11 行至第 16 行的部分是建構函式。在一些程式語言中,會有為了建立物件使用特定的建構子 (constructor),而 Golang 沒有引入額外的新語法,直接以一般的函式充當建構函式來建立物件即可。

第 17 行至第 25 行為外部程式。在我們的 Point 物件 p 中,我們直接存取 p 的屬性 XY,這在物件導向上不是好的習慣,因為我們無法控管屬性,物件可能會產生預期外的行為,比較好的方法,是將屬性隱藏在物件內部,由公開方法去存取。我們在後文中會討論。

類別宣告不限定於結構體

雖然大部分的 Golang 類別都使用結構體,但其實 Golang 類別內部可用其他的型別,如下例:

type Vector []float64                     /*  1 */

func NewVector(args ...float64) Vector {  /*  2 */
        return args                       /*  3 */
}                                         /*  4 */

func WithSize(s int) Vector {             /*  5 */
        v := make([]float64, s)           /*  6 */

        return v                          /*  7 */
}                                         /*  8 */

在第 1 行中,我們宣告 Vector 型態,該型態內部不是使用結構體,而是使用陣列。

我們在第 2 行至第 4 行間及第 5 行至第 8 間宣告了兩個建構函式。由此例可知,Go 不限定建構函式的數量,我們可以視需求使用多個不同的建構函式。

撰寫方法 (Method)

在物件導向程式中,我們很少直接操作屬性 (field),通常會將屬性私有化,再加入相對應的公開方法 (method)。我們將先前的 Point 物件改寫如下:

package main                                  /*  1 */

import (                                      /*  2 */
        "log"                                 /*  3 */
)                                             /*  4 */

// `x` and `y` are private fields.            /*  5 */
type Point struct {                           /*  6 */
        x float64                             /*  7 */
        y float64                             /*  8 */
}                                             /*  9 */

func NewPoint(x float64, y float64) *Point {  /* 10 */
        p := new(Point)                       /* 11 */

        p.SetX(x)                             /* 12 */
        p.SetY(y)                             /* 13 */

        return p                              /* 14 */
}                                             /* 15 */

// The getter of x                            /* 16 */
func (p *Point) X() float64 {                 /* 17 */
        return p.x                            /* 18 */
}                                             /* 19 */

// The getter of y                            /* 20 */
func (p *Point) Y() float64 {                 /* 21 */
        return p.y                            /* 22 */
}                                             /* 23 */

// The setter of x                            /* 24 */
func (p *Point) SetX(x float64) {             /* 25 */
        p.x = x                               /* 26 */
}                                             /* 27 */

// The setter of y                            /* 28 */
func (p *Point) SetY(y float64) {             /* 29 */
        p.y = y                               /* 30 */
}                                             /* 31 */

func main() {                                 /* 32 */
        p := NewPoint(0, 0)                   /* 33 */

        if !(p.X() == 0) {                    /* 34 */
                log.Fatal("Wrong value")      /* 35 */
        }                                     /* 36 */

        if !(p.Y() == 0) {                    /* 37 */
                log.Fatal("Wrong value")      /* 38 */
        }                                     /* 39 */

        p.SetX(3)                             /* 40 */
        p.SetY(4)                             /* 41 */

        if !(p.X() == 3.0) {                  /* 42 */
                log.Fatal("Wrong value")      /* 43 */
        }                                     /* 44 */

        if !(p.Y() == 4.0) {                  /* 45 */
                log.Fatal("Wrong value")      /* 46 */
        }                                     /* 47 */
}                                             /* 48 */

第 6 行至第 9 行是類別宣告的部分。在這個版本的宣告中,我們將 xy 改為小寫,代表該屬性是私有屬性,其可視度僅限於同一 package 中。

第 10 行至第 15 行是 Point 類別的建構函式。請注意我們刻意在第 12 行及第 13 行用該類別的 setters 來初始化屬性,這是刻意的動作。因為我們要確保在設置屬性時的行為保持一致。

第 16 行至第 31 行是 Point 類別的 getters 和 setters。所謂的 getters 和 setters 是用來存取內部屬性的 method。比起直接暴露屬性,使用 getters 和 setters 會有比較好的控制權。日後要修改 getters 或 setters 的實作時,也只要修改同一個地方即可。

在本例中,getters 和 setters 都是公開 method。但 getters 或 setters 不一定必為公開 method。例如,我們想做唯讀的 Point 物件時,就可以把 setters 的部分設為私有 method,留給類別內部使用。

在 Go 語言中,沒有 thisself 這種代表物件的關鍵字,而是由程式設計者自訂代表物件的變數,在本例中,我們用 p 表示物件本身。透過這種帶有物件的函式宣告後,函式會和物件連動;在物件導向中,將這種和物件連動的函式稱為方法 (method)。

雖然在這個例子中,暫時無法直接看出使用方法的好處,比起直接操作屬性,透過私有屬性搭配公開方法帶來許多的益處。例如,如果我們希望 Point 在建立之後是唯讀的,我們只要將 SetXSetY 改為私有方法即可。或者,我們希望限定 Point 所在的範圍為 0.0 至 1000.0,我們可以在 SetXSetY 中檢查參數是否符合我們的要求。

靜態方法 (Static Method)

有些讀者學過 Java 或 C#,可能有聽過過靜態方法 (static method)。這是因為 Java 和 C# 直接將物件導向的概念融入其語法中,然而,為了要讓某些方法在不建立物件時即可使用,所使用的一種補償性的語法機制。由於 Go 語言沒有將物件導向的概念直接加在語法中,不需要用這種語法,直接用頂層函式即可。

例如:我們撰寫一個計算兩點間長度的函式:

package main                                   /*  1 */

import (                                       /*  2 */
        "log"                                  /*  3 */
        "math"                                 /*  4 */
)                                              /*  5 */

type Point struct {                            /*  6 */
        x float64                              /*  7 */
        y float64                              /*  8 */
}                                              /*  9 */

func NewPoint(x float64, y float64) *Point {   /* 10 */
        p := new(Point)                        /* 11 */

        p.SetX(x)                              /* 12 */
        p.SetY(y)                              /* 13 */

        return p                               /* 14 */
}                                              /* 15 */

func (p *Point) X() float64 {                  /* 16 */
        return p.x                             /* 17 */
}                                              /* 18 */

func (p *Point) Y() float64 {                  /* 19 */
        return p.y                             /* 20 */
}                                              /* 21 */

func (p *Point) SetX(x float64) {              /* 22 */
        p.x = x                                /* 23 */
}                                              /* 24 */

func (p *Point) SetY(y float64) {              /* 25 */
        p.y = y                                /* 26 */
}                                              /* 27 */

// Use an ordinary function as static method.  /* 28 */
func Dist(p1 *Point, p2 *Point) float64 {      /* 29 */
        xSqr := math.Pow(p1.X()-p2.X(), 2)     /* 30 */
        ySqr := math.Pow(p1.Y()-p2.Y(), 2)     /* 31 */

        return math.Sqrt(xSqr + ySqr)          /* 32 */
}                                              /* 33 */

func main() {                                  /* 34 */
        p1 := NewPoint(0, 0)                   /* 35 */
        p2 := NewPoint(3.0, 4.0)               /* 36 */

        if !(Dist(p1, p2) == 5.0) {            /* 37 */
                log.Fatal("Wrong value")       /* 38 */
        }                                      /* 39 */
}                                              /* 40 */

本範例和前一節的範例大同小異。主要的差別在於第 29 行至第 33 間多了一個用來計算距離的函式。該函式不綁定特定的物件,相當於 Java 的靜態函式。

因為 Golang 不是 Java 這種純物件導向語言,而是混合命令式和物件式兩種語法,所以不需要使用特定的語法來實踐靜態函式,使用一般的函式即可。

或許有讀者會擔心,使用過多的頂層函式會造成全域空間的汙染和衝突;實際上不需擔心,雖然我們目前將物件和主程式寫在一起,實務上,物件會寫在獨立的package 中,藉由 package 即可大幅減低命名空間衝突的議題。

使用嵌入 (Embedding) 取代繼承 (Inheritance)

繼承 (inheritance) 是一種重用程式碼的方式,透過從父類別 (parent class) 繼承程式碼,子類別 (child class) 可以少寫一些程式碼。此外,對於靜態型別語言來說,繼承也是實現多型 (polymorphism) 的方式。然而,Go 語言卻刻意地拿掉繼承,這是出自於其他語言的經驗。

繼承雖然好用,但也引起許多的問題。像是 C++ 相對自由,可以直接使用多重繼承,但這項特性會引來菱型繼承 (diamond inheritance) 的議題,Java 和 C# 刻意把這個機制去掉,改以介面 (interface) 進行有限制的多重繼承。從過往經驗可知過度地使用繼承,會增加程式碼的複雜度,使得專案難以維護。出自於工程上的考量,Go 捨去繼承這個語法特性。

為了補償沒有繼承的缺失,Go 加入了嵌入 (embedding) 這個新的語法特性,透過嵌入,也可以達到程式碼共享的功能。

例如,我們擴展 Point 類別至三維空間:

package main                                                 /*  1 */

import (                                                     /*  2 */
        "log"                                                /*  3 */
)                                                            /*  4 */

type Point struct {                                          /*  5 */
        x float64                                            /*  6 */
        y float64                                            /*  7 */
}                                                            /*  8 */

func NewPoint(x float64, y float64) *Point {                 /*  9 */
        p := new(Point)                                      /* 10 */

        p.SetX(x)                                            /* 11 */
        p.SetY(y)                                            /* 12 */

        return p                                             /* 13 */
}                                                            /* 14 */

func (p *Point) X() float64 {                                /* 15 */
        return p.x                                           /* 16 */
}                                                            /* 17 */

func (p *Point) Y() float64 {                                /* 18 */
        return p.y                                           /* 19 */
}                                                            /* 20 */

func (p *Point) SetX(x float64) {                            /* 21 */
        p.x = x                                              /* 22 */
}                                                            /* 23 */

func (p *Point) SetY(y float64) {                            /* 24 */
        p.y = y                                              /* 25 */
}                                                            /* 26 */

type Point3D struct {                                        /* 27 */
        // Point is embedded                                 /* 28 */
        Point                                                /* 29 */
        z float64                                            /* 30 */
}                                                            /* 31 */

func NewPoint3D(x float64, y float64, z float64) *Point3D {  /* 32 */
        p := new(Point3D)                                    /* 33 */

        p.SetX(x)                                            /* 34 */
        p.SetY(y)                                            /* 35 */
        p.SetZ(z)                                            /* 36 */

        return p                                             /* 37 */
}                                                            /* 38 */

func (p *Point3D) Z() float64 {                              /* 39 */
        return p.z                                           /* 40 */
}                                                            /* 41 */

func (p *Point3D) SetZ(z float64) {                          /* 42 */
        p.z = z                                              /* 43 */
}                                                            /* 44 */

func main() {                                                /* 45 */
        p := NewPoint3D(1, 2, 3)                             /* 46 */

        // GetX method is from Point                         /* 47 */
        if !(p.X() == 1) {                                   /* 48 */
                log.Fatal("Wrong value")                     /* 49 */
        }                                                    /* 50 */

        // GetY method is from Point                         /* 51 */
        if !(p.Y() == 2) {                                   /* 52 */
                log.Fatal("Wrong value")                     /* 53 */
        }                                                    /* 54 */

        // GetZ method is from Point3D                       /* 55 */
        if !(p.Z() == 3) {                                   /* 56 */
                log.Fatal("Wrong value")                     /* 57 */
        }                                                    /* 58 */
}                                                            /* 59 */

第 5 行至第 26 行是原本的 Point 類別,這和先前的實作是雷同的,不多做說明。

第 27 行至第 44 行是 Point3D 類別,我們來看一下這個類別。

第 27 行至第 31 行是 Point3D 的類別宣告。請注意我們在第 29 行嵌入了 Point 類別。

第 32 行至第 38 行是 Point3d 的建構函式。雖然我們沒有為 Point3D 宣告 SetX()SetY() method,但我們有嵌入 Point 類別,所以我們在第 34 行及第 35 行可以直接使用這些 method。

第 45 行至第 59 行是外部程式的部分。由於我們的 Point3D 內嵌了 Point,雖然 Point3D 沒有自己實作 X()Y() method,我們在第 48 行及第 52 行可直接呼叫這些 method。

在本例中,我們重用了 Point 的方法,再加入 Point3D 特有的方法。實際上的效果等同於繼承。

然而,PointPoint3D 兩者在類別關係上卻是不相干的獨立物件。在以下例子中,我們想將 Point3D 加入 Point 物件組成的切片,而引發程式的錯誤:

// Declare Point and Point3D as above.
 
func main() {
    points := make([]*Point, 0)
 
    p1 := NewPoint(3, 4)
    p2 := NewPoint3D(1, 2, 3)
 
    // Error!
    points = append(points, p1, p2)
}

在 Go 語言中,需要使用介面 (interface) 來解決這個議題,這就是我們下一篇文章所要探討的主題。

嵌入指標

除了嵌入其他結構外,結構也可以嵌入指標。我們將上例改寫如下:

package main
 
import (
    "log"
)
 
type Point struct {
    x float64
    y float64
}
 
func NewPoint(x float64, y float64) *Point {
    p := new(Point)
 
    p.SetX(x)
    p.SetY(y)
 
    return p
}
 
func (p *Point) X() float64 {
    return p.x
}
 
func (p *Point) Y() float64 {
    return p.y
}
 
func (p *Point) SetX(x float64) {
    p.x = x
}
 
func (p *Point) SetY(y float64) {
    p.y = y
}
 
type Point3D struct {
    // Point is embedded as a pointer
    *Point
    z float64
}
 
func NewPoint3D(x float64, y float64, z float64) *Point3D {
    p := new(Point3D)
 
    // Forward promotion
    p.Point = NewPoint(x, y)
 
    // Forward promotion
    p.Point.SetX(x)
    p.Point.SetY(y)
 
    p.SetZ(z)
 
    return p
}
 
func (p *Point3D) Z() float64 {
    return p.z
}
 
func (p *Point3D) SetZ(z float64) {
    p.z = z
}
 
func main() {
    p := NewPoint3D(1, 2, 3)
 
    // GetX method is from Point
    if !(p.X() == 1) {
        log.Fatal("Wrong value")
    }
 
    // GetY method is from Point
    if !(p.Y() == 2) {
        log.Fatal("Wrong value")
    }
 
    // GetZ method is from Point3D
    if !(p.Z() == 3) {
        log.Fatal("Wrong value")
    }
}

同樣地,仍然不能透過嵌入指楆讓型別直接互通,而需要透過介面 (interface)。

結語

在本文中,我們介紹了 Golang 的物件系統。相較於 C++ 或 Java 或 C#,Golang 的物件系統相對比較輕量,儘量不使用新的保留字,而用現用的語法來實現物件的特性。

Golang 的物件系統刻意拿掉繼承,改用嵌入來重用程式碼,這是由先前的程式語言中學習到的經驗和教訓。但嵌入無法實踐子類別 (subtyping),這個問題要等到我們下一篇講到的介面 (interface) 才有解。


沒有 object、沒有 class 、沒有繼承的 Go, 靠著 struct / method / interface, 好像也享有 OOP 語言的優點呢

method

本來以為 Go 是物件導向,後來發現沒有 class! 基本上使用 struct 與 method 來達到類似的效果。

method 是一個有 receiver argument 的 function we can define method on a type. (不一定是 struct,但這個 type 要在同個 package 中,int 這些 built-in type 要先透過 type 關鍵字來定義一個新型別才能用,例如 type myint int

// 定義 Vertex struct
type Vertex struct {
	X, Y float64
}
// Abs method
func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
    // 使用 method 時就像別的語言使用一個 class 內ㄉ function 一樣
	fmt.Println(v.Abs())
}
  • receiver argument 的型別很重要!golang 會依據他的型別幫忙轉~所以如果這個 method 要改值,記得在 receiver 那邊寫好是吃 pointer(打星星)
// v *Vertex 這樣就算下面的 v 並不是一個 pointer,go 也會幫忙轉 &v
func (v *Vertex) Scale(f float64) { 
	v.X = v.X * f
	v.Y = v.Y * f
}
func main() {
    // 如果拿掉上面 receiver 的*,也可以在這邊 &Vertex{3, 4}
	v := Vertex{3, 4}
    // 或是 (&v).Scale(10)
	v.Scale(10)
}

interface

Go 裡 interface 是一個型別,裡面有定義一堆 method signatures, 只要合乎這些簽章的數值(通常是 struct)就可以放進這個介面變數。 如果這個變數沒有實作規定的 method 的話,就會噴錯。

  • empty interface 沒有定義任何 method 的 interface 當作 input 的型別,就可以接受任意型別的 input。

以下例子來自day15 - 介面(續) empty interface + 以 type 為不同 case 的 switch

func main() {
	printAnyType(2020)
	printAnyType("Iron Man")
	printAnyType(0.25)
}

// 定義一個函式,接收任何型別,並且格式化輸出值
func printAnyType(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("case int: %d \n", v)
	case string:
		fmt.Printf("case string: %s \n", v)
	default:
		fmt.Printf("default: %v \n", v)
	}
}

沒有繼承

畢竟沒有 class,也沒有繼承的概念。 而是使用 struct 中包 struct,稱之為 composition。 go 中還能使用 embbeded,這裡不打了。

在物件導向程式中,通常會用繼承來共享上層元件的程式碼。然而,go語言沒有繼承的特性,但我們能用組合的方式來共享程式碼。不僅如此,go語言還提供一種優於組合的語法特性,稱作內嵌。

組合(composition)

先來談談我所知道的組合,大部分的文章會講到組合是聚合(aggregation)的一種,而它們都是源自於UML的產物,實際上UML定義的定義很模糊也很難理解。因此,我要講的是它們最基本的一面,也就是 Is-AHas-A 關係:

  • Is-A: 繼承關係,表示一個物件也是另一個物件。
  • Has-A: 組合關係,表示一個物件擁有另一個物件。

很多文章和書都建議我們要多用組合少用繼承,這是因為繼承會對物件造成巨大的依賴關係。我們用一個範例來說明組合:

// 定義一個英雄結構,包含了正常人結構
type Hero struct {
	Person   *Person
	HeroName string
	HerkRank int
}

// 定義一個正常人結構
type Person struct {
	Name string
}

func main() {
	var tony = &Hero{&Person{"Tony Stark"}, "Iron Man", 1}
	fmt.Printf("Hero=%+v\n", *tony)
	fmt.Printf("Person=%+v\n", *(tony.Person))
}

執行結果:

Hero={Person:0xc0000841e0 HeroName:Iron Man HerkRank:1}
Person={Name:Tony Stark}

上面範例中,我們看到了所謂的組合就是結構再包結構的概念,透過這樣的方式共享結構資料或方法。

內嵌(Embedding)

再來談談go語言的內嵌特性,這個特性並沒有寫在A Tour of Go,而是在Effective Go裡頭。

Effective Go: Embedding

Go語言的內嵌其實就是組合的概念,只是它更加簡潔及強大。內嵌允許我們在結構內組合其他結構時,不需要定義欄位名稱,並且能直接透過該結構叫用欄位或方法。我們將上面的範例改成使用內嵌,如下:

// 定義一個英雄結構
type Hero struct {
	*Person   // 不需要欄位名稱
	HeroName string
	HerkRank int
}

// 定義一個正常人結構
type Person struct {
	Name string
}

func main() {
	var tony = &Hero{
		&Person{"Tony Stark"},
		"Iron Man",
		1}

	fmt.Printf("%s\n", tony.Name)    // 直接叫用內部結構資料
	// 等於 fmt.Printf("%s\n", tony.Person.Name)
}
// 執行結果: Tony Stark

實際上,內嵌的結構欄位還是會有名稱,就是和結構本身的名稱同名。

另外,上面範例是用匿名初始化,也可以使用具名初始化,差別在於初始化參數的數量和順序是可以被調整的:

var tony = &Hero{
		Person:   &Person{"Tony Stark"},
		HeroName: "Iron Man",
		HeroRank: 1}

內嵌與方法

上面看到的範例都是內嵌結構資料,現在我們來試試看內嵌結構方法,修改同一個範例如下:

// 定義一個英雄結構
type Hero struct {
	*Person
	HeroName string
	HeroRank int
}

// 英雄都會飛
func (*Hero) Fly() {
	fmt.Println("I can fly.")
}

// 定義一個正常人結構
type Person struct {
	Name string
}

// 正常人會走路
func (*Person) Walk() {
	fmt.Println("I can walk.")
}

func main() {
	var tony = &Hero{
		Person:   &Person{"Tony Stark"},
		HeroName: "Iron Man",
		HeroRank: 1}

	tony.Walk()   // 等於 tony.Person.Walk()
	tony.Fly()
}

執行結果:

I can walk.
I can fly.

內嵌結構欄位同名

當有多個內嵌結構時,就有可能發生欄位同名的問題。我們稍微修改一下範例,超級英雄也會想養一隻寵物,這很合理的。因此,我們就加入一個寵物結構:

// 定義一個英雄結構
type Hero struct {
	*Person
	*Pet
	HeroName string
	HeroRank int
}

// 定義一個正常人結構
type Person struct {
	Name string
}

// 定義一個寵物結構
type Pet struct {
	Name string
}

func main() {
	var tony = &Hero{
		Person:   &Person{"Tony Stark"},
		Pet:   &Pet{"Pepper"},
		HeroName: "Iron Man",
		HeroRank: 1}

	fmt.Printf("%s\n", tony.Name)
}

由於 Person 和 Parner 都有 Name 這個欄位,直接叫用 tony.Name 就會產生衝突,編譯器會顯示錯誤訊息:

./main.go:40:25: ambiguous selector tony.Name

內嵌其他型別

事實上,可以被內嵌的型別不只有結構,也可以是基本型別,範例如下:

type Data struct {
	int
	string
	float32
	bool
}

func main() {
	var data = &Data{1, "Iron Man", 1.2, true}
	fmt.Println(*data)
	fmt.Printf("%+v \n", *data)
}

執行結果

{1 Iron Man 1.2 true}
{int:1 string:Iron Man float32:1.2 bool:true} 

基本型別被內嵌之後,欄位名稱就是型別的原始名稱,ex: int, string, ...。

小結

今天介紹了go語言的內嵌特性,使得沒有繼承的go語言,依然可以相互共享結構內的程式碼。而這樣的作法在實務上究竟是否優於繼承,可能需要寫久一點,才會深刻了解。

Go 簡單例子來理解 sync.Mutex 和 sync.RWMutex

出處: https://clouding.city/go/mutex-rwmutex/

用簡單的例子來理解 sync.Mutexsync.RWMutex

蓋一間銀行

假設有一間銀行,可以存款和查詢餘額。

package main

import (
    "fmt"
)

type Bank struct {
    balance int
}

func (b *Bank) Deposit(amount int) {
    b.balance += amount
}

func (b *Bank) Balance() int {
    return b.balance
}

func main() {
    b := &Bank{}

    b.Deposit(1000)
    b.Deposit(1000)
    b.Deposit(1000)

    fmt.Println(b.Balance())
}
$ go run main.go
3000

執行之後結果是 3000 沒問題,1000+1000+1000=3000。

同時存款

銀行不太可能讓人一個一個排隊存款,也需要支援同時存款,當今天存款的動作是並行的,會發生什麼事呢?

這邊用 sync.WaitGroup 去等待所有 goroutine 執行完畢,之後再印出餘額。

func main() {
    var wg sync.WaitGroup
    b := &Bank{}

    wg.Add(3)
    go func() {
        b.Deposit(1000)
        wg.Done()
    }()
    go func() {
        b.Deposit(1000)
        wg.Done()
    }()
    go func() {
        b.Deposit(1000)
        wg.Done()
    }()

    wg.Wait()

    fmt.Println(b.Balance())
}
$ go run main.go
3000

還是 3000 沒問題,那我們同時存款 1000 次的時候會發生什麼事呢?

func main() {
    var wg sync.WaitGroup
    b := &Bank{}

    n := 1000
    wg.Add(n)
    for i := 1; i <= n; i++ {
        go func() {
            b.Deposit(1000)
            wg.Done()
        }()
    }

    fmt.Println(b.Balance())
}
$ go run main.go
946000

誒奇怪,正常來說 1000 * 1000 = 1000000 嗎?怎麼數字不正確!

我們這次多帶一個參數 -race 跑看看

-race 參數是 go 的 Race Detector,內建整合工具,可以輕鬆檢查出是否有 race condition

$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c00009e010 by goroutine 8:
  main.main.func1()
      .../main.go:15 +0x6f

Previous write at 0x00c00009e010 by goroutine 7:
  main.main.func1()
      .../main.go:15 +0x85

Goroutine 8 (running) created at:
  main.main()
      .../main.go:31 +0xf4

Goroutine 7 (finished) created at:
  main.main()
      .../main.go:31 +0xf4
==================
996000
Found 1 data race(s)
exit status 66

喔喔喔發現原來有 race condition, 因為同時去對 Bank.balance 去做存取的動作,數量少的時候可能沒問題,當量大的時候就可能出錯。

sync.Mutex

為了防止這種狀況發生,就可以用互斥鎖 sync.Mutex 來處理這個問題,同時間只有一個 goroutine 能存取該變數。

這次我們在 Deposit() 存款前先 Lock(),存款後再 Unlock()

type Bank struct {
    balance int
    mux     sync.Mutex
}

func (b *Bank) Deposit(amount int) {
    b.mux.Lock()
    b.balance += amount
    b.mux.Unlock()
}

func (b *Bank) Balance() int {
    return b.balance
}
$ go run -race main.go
1000000

這次結果正確了,而且也沒跳出 race condition 的警訊。

同時存款和查詢

想當然會有多人一起存款,就會有多人一起查詢餘額。也會有多人一起運動

多加一組查詢 1000 次的 goroutine 再執行看看。

func main() {
    var wg sync.WaitGroup

    b := &Bank{}

    n := 1000
    wg.Add(n)
    for i := 1; i <= n; i++ {
        go func() {
            b.Deposit(1000)
            wg.Done()
        }()
    }
    wg.Add(n)
    for i := 1; i <= n; i++ {
        go func() {
            _ = b.Balance()
            wg.Done()
        }()
    }

    wg.Wait()

    fmt.Println(b.Balance())
}
$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c0000180e0 by goroutine 59:
  main.main.func2()
      .../main.go:22 +0x6f

Previous write at 0x00c0000180e0 by goroutine 58:
  main.(*Bank).Deposit()
      .../main.go:15 +0x70
  main.main.func1()
      .../main.go:35 +0x75

Goroutine 59 (running) created at:
  main.main()
      .../main.go:40 +0x153

Goroutine 58 (finished) created at:
  main.main()
      .../main.go:33 +0xf4
==================
==================
WARNING: DATA RACE
Read at 0x00c0000180e0 by goroutine 60:
  main.main.func2()
      .../main.go:22 +0x6f

Previous write at 0x00c0000180e0 by goroutine 58:
  main.(*Bank).Deposit()
      .../main.go:15 +0x70
  main.main.func1()
      .../main.go:35 +0x75

Goroutine 60 (running) created at:
  main.main()
      .../main.go:40 +0x153

Goroutine 58 (finished) created at:
  main.main()
      .../main.go:33 +0xf4
==================
1000000
Found 2 data race(s)
exit status 66

不意外,因為同時對 balance 去做讀寫,當然跳出 race condition 的警告。

我們一樣在 Balance() 加上 Lock()Unlock() 後執行。

type Bank struct {
    balance int
    mux     sync.Mutex
}

func (b *Bank) Deposit(amount int) {
    b.mux.Lock()
    b.balance += amount
    b.mux.Unlock()
}

func (b *Bank) Balance() (balnce int) {
    b.mux.Lock()
    balance = b.balance
    b.mux.Unlock()
    return 
}
$ go run -race main.go
1000000

結果成功了,也沒有 race 的警告了。

讀寫互相阻塞

目前這邊看起來都還不錯,但以現在的情況來說,只要有人讀,或只要有人寫,就會被 block。

假如銀行存款和查詢各要上花一秒:

package main

import (
    "log"
    "sync"
    "time"
)

type Bank struct {
    balance int
    mux     sync.Mutex
}

func (b *Bank) Deposit(amount int) {
    b.mux.Lock()
    time.Sleep(time.Second) // spend 1 second
    b.balance += amount
    b.mux.Unlock()
}

func (b *Bank) Balance() (balance int) {
    b.mux.Lock()
    time.Sleep(time.Second) // spend 1 second
    balance = b.balance
    b.mux.Unlock()
    return 
}

func main() {
    var wg sync.WaitGroup
    b := &Bank{}

    n := 5
    wg.Add(n)
    for i := 1; i <= n; i++ {
        go func() {
            b.Deposit(1000)
            log.Printf("Write: deposit amonut: %v", 1000)
            wg.Done()
        }()
    }
    wg.Add(n)
    for i := 1; i <= n; i++ {
        go func() {
            log.Printf("Read: balance: %v", b.Balance())
            wg.Done()
        }()
    }

    wg.Wait()
}
$ go run -race main.go
2020/05/02 02:11:24 Write: deposit amonut: 1000
2020/05/02 02:11:25 Write: deposit amonut: 1000
2020/05/02 02:11:26 Write: deposit amonut: 1000
2020/05/02 02:11:27 Write: deposit amonut: 1000
2020/05/02 02:11:28 Write: deposit amonut: 1000
2020/05/02 02:11:29 Read: balance: 5000
2020/05/02 02:11:30 Read: balance: 5000
2020/05/02 02:11:31 Read: balance: 5000
2020/05/02 02:11:32 Read: balance: 5000
2020/05/02 02:11:33 Read: balance: 5000

就會發現,每隔一秒才能處理一個 action,以各五次讀寫來說,總共就要花上 10 秒,但對讀來說,應該可以瘋狂讀,每次讀都會是安全的, 值也都會是一樣,除非當下有寫的動作,它不應該被其他讀的動作 block。

sync.RWMutex

sync.RWMutex 是一個讀寫鎖(multiple readers, single writer lock),多讀單寫,可以允許多個讀並發,單個寫。

sync.Mutex 換成 sync.RWMutex

type Bank struct {
    balance int
    mux     sync.RWMutex    // read write lock
}

func (b *Bank) Deposit(amount int) {
    b.mux.Lock()            // write lock
    time.Sleep(time.Second)
    b.balance += amount
    b.mux.Unlock()          // wirte unlock
}

func (b *Bank) Balance() (balance int) {
    b.mux.RWLock()          // read lock
    time.Sleep(time.Second)
    balance = b.balance
    b.mux.RWUnlock()        // read unlock
    return 
}
$ go run -race main.go
2020/05/02 02:13:59 Write: deposit amonut: 1000
2020/05/02 02:14:00 Read: balance: 1000
2020/05/02 02:14:00 Read: balance: 1000
2020/05/02 02:14:00 Read: balance: 1000
2020/05/02 02:14:00 Read: balance: 1000
2020/05/02 02:14:00 Read: balance: 1000
2020/05/02 02:14:01 Write: deposit amonut: 1000
2020/05/02 02:14:02 Write: deposit amonut: 1000
2020/05/02 02:14:03 Write: deposit amonut: 1000
2020/05/02 02:14:04 Write: deposit amonut: 1000

執行之後會發現,本來要花 10 秒,已經縮短成 5 秒了,只要當下是讀的時候,都會同時進行,並不會互相影響,寫的時候就會 block 讀和寫,只有一個寫會發生。

總結

  1. 在寫 goroutine 的時候,需要考慮 race condition,在執行或測試上可以加上 -race 去檢查,以免結果與預期不符
  2. 遇到 race condition 的時候可以考慮用 sync.Mutex 來解決,有讀寫阻塞的時候可以用 sync.RWMutex
  3. syncRWMutex 可以有同時允許多個 RLockRUnlock 但只能有一個 LockUnlock

Golang - 深入理解 interface 常見用法

出處: https://blog.kennycoder.io/2020/02/03/Golang-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3interface%E5%B8%B8%E8%A6%8B%E7%94%A8%E6%B3%95/

此篇文章介紹在 Golang 中 interface 的常見用法,interface 在 Golang 中是一個很重要的環節。interface 可以拿來實現多種用途,請看介紹。

interface 定義

interface 又稱接口,其實功能有點類似於 Java 中的 interface,但是在一些地方完全不同於 Java 中 interface 的設計。

在 Golang 中,interface 其中一個功能就是可以使用 interface 定義行為,也就是說 interface 中可以定義一些方法來表示一個對象的行為,而當我們有自定義的型態假設想要擁有這些行為,就是去實踐 interface 裡面的方法。

interface 定義行為

來看個例子:

package main

import "fmt"

type Animal interface {
	Eat()
	Run()
}

type Dog struct {
	Name string
}

func (d *Dog) Eat() {
	fmt.Printf("%s is eating\n", d.Name)
}

func (d *Dog) Run() {
	fmt.Printf("%s is running\n", d.Name)
}

func ShowEat(animal Animal) {
	animal.Eat()
}

func ShowRun(animal Animal) {
	animal.Run()
}

func main() {
	dog := Dog{Name:"Kenny"}
	ShowEat(&dog)
	ShowRun(&dog)
}
  1. 建立一個 Animal 型態的 interface,其定義了 Eat () 跟 Run (),來表達動物都會擁有的行為。
  2. 建立一個 Dog Struct,並且實踐 interface 裡面的 Eat () 跟 Run ()。要注意的是,由於在實作 EatRun 方法時,都是用指標 (d *Dog) ,這個所代表的意思是透過傳遞指標來操控同一個 Struct 實例,如果沒有用指標則會導致淺複製的行為,並不是操控同一個 Struct。
  3. 建立 ShowEat、ShowRun 方法,並且參數型態用 Animal。

以上的程式碼可以得知以下兩件事情:

  1. 在 Golang 中如果自定義型態實現了 interface 的所有方法,那麼它就會認定該自定義型態也是 interface 型態的一種。也就是所謂的鴨子型別 (Duck typing) 的實現。只要你有符合這些種種的行為,即使你不是真的鴨子,那麼還是會認定你是一隻鴨子。
  2. 透過 ShowRun 跟 ShowEat () 得知實現了多型的行為。所謂多型的意思是相同的訊息給予不同的物件會引發不同的動作,因為參數型態用 Animal 所以,每個動物會有各自的吃跟跑的行為,執行出來的結果也會各自不一樣。

interface 型態與值

將 main 裡面程式碼改成這樣:

func main() {
	var animal Animal
	fmt.Println(animal)
}

這樣輸出會是 nil。

  1. 這代表著 animal 在底層儲存的型態為 nil。interface 類型默認是一個指針 (引用類型),如果沒有對 interface 初始化就使用,那麼會輸出 nil

  2. 但是我們可以指定自定義型態給 nil interface,如果該自定義型態有實現該 interface 方法即可。

    func main() {
    	var animal Animal
    	animal = &Dog{Name:"Kenny"}
    	fmt.Println(animal)
    }
    

    運行結果為 &{Kenny}

    也就是說 animal 底層儲存的型態會 *Dog,而值是 Dog 結構實例的位址值。

interface 繼承

一個自定義型態是可以實現多個 interface 的。此外,interface 也可以繼承別的 interface 的行為:

package main

import "fmt"

type Eater interface {
	Eat()
}

type Runner interface {
	Run()
}

type Animal interface {
	Eater
	Runner
}

type Dog struct {
	Name string
}

func (d *Dog) Eat() {
	fmt.Printf("%s is eating\n", d.Name)
}

func (d *Dog) Run() {
	fmt.Printf("%s is running\n", d.Name)
}

func ShowEat(animal Animal) {
	animal.Eat()
}

func ShowRun(animal Animal) {
	animal.Run()
}

func ShowEat2(eater Eater) {
	eater.Eat()
}

func ShowRun2(runner Runner) {
	runner.Run()
}

func main() {
	dog := Dog{Name:"Kenny"}
	ShowEat(&dog)
	ShowRun(&dog)
	ShowEat2(&dog)
	ShowRun2(&dog)
}

在 Animal interface 透過內嵌的方式,將 Eater interface、Runner interface 定義的行為放進去。

這樣的話 Dog Struct 必須都實現 Eat () 跟 Run () 才能是 Animal 的一種。此外,因為這樣做也代表,Dog Struct 也是 Eater 及 Runner 的一種。

所以看到定義的 ShowEat2 () 跟 ShowRun2 () 皆能接受 Dog Struct。

透過 interface 儲存異質陣列或 slice

前面說過 Golang 會檢查類型的實例,是否都有實現 interface 定義的行為,如果是的話就可以接受介面型態是不同型態實例的指定。

透過這種特性,假設我們有個需求是一個陣列或 slice 存放的型態無法事先確定,且每個元素的型態可能都不是一樣,就可以透過 interface 來解決!

package main

import "fmt"

type Eater interface {
	Eat()
}

type Runner interface {
	Run()
}

type Animal interface {
	Eater
	Runner
}

type Dog struct {
	Name string
}

func (d *Dog) Eat() {
	fmt.Printf("%s is eating\n", d.Name)
}

func (d *Dog) Run() {
	fmt.Printf("%s is running\n", d.Name)
}

type Cat struct {
	Name string
}

func (c *Cat) Eat() {
	fmt.Printf("%s is eating\n", c.Name)
}

func (c *Cat) Run() {
	fmt.Printf("%s is running\n", c.Name)
}

func ShowEat(animal Animal) {
	animal.Eat()
}

func ShowRun(animal Animal) {
	animal.Run()
}

func ShowEat2(eater Eater) {
	eater.Eat()
}

func ShowRun2(runner Runner) {
	runner.Run()
}

func main() {
	animals := [...]Animal{
		&Dog{Name:"Kenny"},
		&Cat{Name:"Nicole"},
	}

	for _, animal := range animals {
		fmt.Println(animal)
	}

	instances := [...]interface{}{
		123,
		"Hello World",
		&Dog{Name:"Kenny"},
		&Cat{Name:"Nicole"},
	}

	for _, instance := range instances {
		fmt.Println(instance)
	}
}

在這邊多定義了 Cat Struct,並且同樣實現了 Animal interface。

因此在前面可以建立 Animal 型態的陣列,裡面可以放不同結構體的實例,只要裡面放置的結構體有實現 Animal interface 行為,就會被當作 Animal 實例。

而第二個例子是利用空接口型態,裡面可以放置各種型態的元素,以這個例子來看既能放 int、string、Dog Struct、Cat Struct。

也因為空接口的特性也是實現泛型的重要關鍵。

型態斷言

但是根據以上的例子會發現一個問題:

假設利用自定義型態為 Animal interface 指定型態的話,該型態就能存取 interface 的行為,並不能存取自定義型態的屬性及其它自定義型態的方法。

這時候可以利用 Golang 提供的型態斷言的特性,請看:

func main() {
	var animal Animal
	animal = &Dog{Name:"Kenny"}
	dog := animal.(*Dog)
	fmt.Println(dog.Name)

	animal = &Cat{Name:"Nicole"}
	cat := animal.(*Dog)
	fmt.Println(cat.Name)
}

透過.(type) 的方式來斷定該接口實際上是存放哪個實例。但是有個缺點是如果型態判斷錯誤,會直接造成 panic。

出現以下錯誤訊息:

panic: interface conversion: main.Animal is *main.Cat, not *main.Dog

這是因為後面的 animal 是指定 Cat 實例,結果後面型態斷言用 Dog,會造成執行時期的錯誤。

要怎麼避免呢?

可以透過 switch 的方式一一去判斷型態:

func main() {
	animals := [...]Animal{
			&Dog{Name:"Kenny"},
			&Cat{Name:"Nicole"},
		}

	for _, animal := range animals {
		switch animal.(type) {
		case *Dog:
			fmt.Println(animal.(*Dog).Name)
		case *Cat:
			fmt.Println(animal.(*Cat).Name)
		default:
			fmt.Println("you are not animal!!")
		}
	}
}

透過.(type) 來一一比對出對的型態。

此外,Golang 型態斷言也提供了檢測機制:

func main() {
	animals := [...]Animal{
			&Dog{Name:"Kenny"},
			&Cat{Name:"Nicole"},
		}

	for _, animal := range animals {
		if dog, ok := animal.(*Dog); ok {
			fmt.Println(dog.Name)
		}

		if cat, ok := animal.(*Cat); ok {
			fmt.Println(cat.Name)
		}
	}
}

當然了,如果斷言的形態越多用 switch 相對可讀性會較高。

空 interface 的限制

根據以上的例子可以空接口提供很多便利性,但是也有其限制:

一個空的接口會隱藏值對應的表示方式和所有的公開的方法,必須使用類型斷言才能來來訪問內部的值,如果事先不知道空接口指向的值的具體類型,就無法操作。

為此,才需要 reflect 機制,可以知道一個接口類型的變量具體是什麼(什麼類型),有什麼能力(有哪些方法)。這也是在寫 Golang 程式庫常常會用到的特性,因為有 interface 可以實現泛型的特性,有了泛型的特性又可以透過 reflect 機制來促發其不同型態的屬性及方法。

總結

  • Golang interface 重點是「行為」,不管定義的介面型態是什麼,只要行為符合就屬於該介面型態的一種。
  • Golang interface 可以說是動態語言鴨子型別的展現。
  • 利用 interface 可實現泛型、多型的功能,從而可以調用同一個函數名的函數但實現完全不同的功能。

所以根據以上 interface 的特點,在看看 Golang 的標準程式庫裡面運用大量的 interface 的特性來完成,例如標準程式庫定義檔案讀寫的 Reader、Writer interface:

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

利用 os.File 實現了 Reader、Writer interface,來實作檔案讀寫的實現。


Interface 接口

要特別注意,這個interface跟其他語言中的定義與作用會不太一樣。

首先讓我們回憶一下golang的特性,會想起他是「輕量級的物件導向」,也就是他沒有完全實作物件導向的所有特徵。具體來說,golang沒有class(類別)與繼承(這樣還能稱之為物件導向嗎?)。但是現代軟體開發,如果需要類似「多型」的需求怎麼辦呢?interface就是golang中用來實踐多型的利器,雖然並不是完全符合多型的概念,但至少在概念上是接近的。尤其是在golang這種強型別的語言,interface可以發揮更高的潛能。

interface有兩種,分別是型態與定義。interface可以代表任何型態,這在開發上可以帶來極大的便利(弱型別語言表示),我們來看看具體上要如何實作:

func Hello(value interface{}) {  
}

將interface作為參數宣告,這個函數就可以接受任意型態的參數。但在實際使用之前,我們還是必須先辨別傳遞進來的參數型別,才能做接下來的邏輯實作,畢竟golang依然是個強型別的語言,沒有因為有了interface就做出讓步。

func Hello(value interface{}) {  
    // 透過型態斷言揭露 interface{} 真正的型態。
    switch v := value.(type) {
        // 如果 value 是字串型態。
        case string:
            fmt.Println("value 是字串,內容是 " + v)
        // 如果 value 是 int 型態。
        case int:
            fmt.Printf("value 是數值,加上二就是 %d", v + 2)
    }
}

延續第一個範例,我們從外部得到型別未知的參數value,透過switchvalue.(type)方法,可以將不同型別的邏輯分離出來,做不同的處理。如果你很確定參數的型別,也可以直接使用宣告的方式來取代switch判斷,方法如下:

func Hello(value interface{}) {  
    fmt.Println("value 是字串,內容是 " + value.(string))
}

但如果interface參數與你預期的型別不同,會出現panic警告。作為參數可以為任意類別,如果函數的返回值為interface,也就代表函數可以返回任意類別值。

在變數的方面,當我們定義一個空的interface,它可以指定為任意型別

var a interface{}
var i int = 5
s := "Hello world"
// a可以儲存任意類別的值
a = i
a = s

到這邊你可能會想,一個強型別語言為什麼需要想辦法實作一個可以是任何型態的參數或變數,這不是根本否定的強型別的價值嗎?我想到一個長久以來在工程師圈關於「限制-自由」的拉扯,有一句名言是這樣總結的:

限制帶給你新的自由

限制的好處是當我們在規則與紀律之中妥協,我們可以更早發現不協調之處,讓bug無所遁形。如果毫無限制邊界,反而讓人無從發揮。我認為這並不是一個布林的問題,而是float,在光譜之中有眾多選擇,可以讓每個人依喜好與需求做選擇。

這樣的設計讓golang在大部份的時候受到型別拘束保護,不會產生型別的意外狀況;當在需要開發套件與第三方程式串接的時候,又不需要把自己綁死侷限了開發空間的可能,是語言設計者的一個優雅的權衡。

我們來看另一個例子,如何在golang中用interface實現多型:

package main

import (
	"fmt"
)

type Animal interface {
	Speak() string
}

type Dog struct {
}

func (d Dog) Speak() string {
	return "Woof!"
}

type Cat struct {
}

func (c Cat) Speak() string {
	return "Meow!"
}

type Pikachu struct {
}

func (p Pikachu) Speak() string {
	return "Pika pika!"
}

type Programmer struct {
}

func (j Programmer) Speak() string {
	return "Design patterns!"
}
func main() {
	animals := []Animal{Dog{}, Cat{}, Pikachu{}, Programmer{}}
	for _, animal := range animals {
		fmt.Println(animal.Speak())
	}
}

如果執行這段程式,我們會得到:

Woof!
Meow!
Pika pika!
Design patterns!

Animal作為一個interface定義一個空的Speak()方法,藉由宣告一個Animal陣列animals將貓、狗、皮卡丘、工程師實體傳進陣列中,接著實體各自執行自己實作的Speak()方法。


struct

struct 用來自定義複雜資料結構,可以包含多個欄位(屬性),可以巢狀;go中的struct型別理解為類,可以定義方法,和函式定義有些許區別;struct型別是值型別。

package main

type User struct {
	Name string
	Age  int32
	mess string
}

var user User
var user1 *User = &User{}
var user2 *User = new(User)

struct的方法

在go語言中,我們可以為自定義型別定義型別相關的方法,比如:

func (p *player) Name() string {
	return p.name
}

上面的程式碼為player這個自定義型別宣告一個名為Name的方法,該方法返回一個string。值得注意的是(p *player)這段程式碼指定了我們是為player建立方法,並將呼叫該方法的例項指標當作變數p傳入該函式,如果沒有(p *player)這段程式碼,這個方法就變成了一個普通的全域性函式。

struct的嵌入(Embedding)

go語言中的“繼承”和其他語言中的繼承有很大區別,比如:

type player struct {
	User
}

這是一種繼承的寫法,在go語言中這種方式叫做嵌入(embed),此時player型別就擁有了User型別的Name, Age, mess 等變數

struct的tag

這種方式主要是用在xml,json和struct間相互轉換,非常方便直觀,比如介面給的引數一般是json傳過來,但是內部我們要轉為struct再進行處理。

package main

import "encoding/json"
import "fmt"

type User struct {
	Name string `json:"userName"`
	Age  int    `json:"userAge"`
}

func main() {
	var user User
	user.Name = "nick"
	user.Age = 18
	conJson, _ := json.Marshal(user)
	fmt.Println(string(conJson)) //{"userName":"nick","userAge":0}
}

interface

golang不支援完整的物件導向思想,它沒有繼承,多型則完全依賴介面實現。golang只能模擬繼承,其本質是組合,只不過golang語言為我們提供了一些語法糖使其看起來達到了繼承的效果。Golang中的介面,不需要顯示的實現。Interface型別可以定義一組方法,但是這些不需要實現。並且interface不能包含任何變數。只要一個變數,含有介面型別中的所有方法,那麼這個變數就實現這個介面。因此,golang中沒有implement類似的關鍵字;如果一個變數含有了一個interface型別的多個方法,那麼這個變數就實現了多個介面;如果一個變數只含有了一個interface的方部分方法,那麼這個變數沒有實現這個介面。

interface的定義

interface型別預設是一個指標。

package main

type Car interface {
	NameGet() string
	Run(n int)
	Stop()
}

空介面 Interface{}:空介面沒有任何方法,所以所有型別都實現了空介面。

var a int
var b interface{} //空介面
b = a

interface的多型

一種事物的多種形態,都可以按照統一的介面進行操作。這種方式是用的最多的,有點像c 中的類繼承。

package main

type Item interface {
	Name() string
	Price() float64
}
type VegBurger struct {
}

func (r *VegBurger) Name() string {
	return "vegburger"
}
func (r *VegBurger) Price() float64 {
	return 1.5
}

type ChickenBurger struct {
}

func (r *ChickenBurger) Name() string {
	return "chickenburger"
}
func (r *ChickenBurger) Price() float64 {
	return 5.5
}

Interface巢狀

一個介面可以巢狀在另外的介面。即需要實現2個介面的方法。在下面的例子中Used就包含了Car這個介面的所有方法。

package main

type Car interface {
	NameGet() string
	Run(n int)
	Stop()
}
type Used interface {
	Car
	Cheap()
}

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對指令碼之家的支援。

map[string]interface操作

package main

import "fmt"

func main() {
    datalist := make(map[string]interface{}, 0)
	fmt.Println(datalist)

	data := []string{"test1", "test2", "test3"}

	for _, name := range data {
		datalist[name] = true
	}

	fmt.Println(datalist)
}

LiveKit 環境搭建步驟整理

1. 準備工作

環境要求

  • Go 1.22+ 版本
  • GOPATH/bin 在 PATH 環境變量中

2. 下載源碼

git clone https://github.com/livekit/livekit.git
cd livekit

3. 編譯方式

命令行編譯

# 進入源碼目錄
cd livekit

# 通過 mage 編譯
./bootstrap.sh
mage

編譯完成後,會在 bin 目錄下生成可執行程序 livekit-server

4. 安裝 LiveKit CLI

下載並安裝 CLI

git clone https://github.com/livekit/livekit-cli
cd livekit-cli
make install

5. 生成訪問秘鑰和 Token

生成 Token 命令

lk create-token \
--api-key 356APISejy567Mgg9X7wYzw \
--api-secret 2ll78HjY2MvB2yGSCueswesd28GnuhjGN4c02JuijhclQ \
--join --room my-first-room --identity user1 \
--valid-for 24h

注意事項

  • API key 和 secret 可以自己隨便填寫
  • 房間名 my-first-room 隨機填寫
  • 用戶名 user1,建議生成兩個用戶(user1 和 user2)以便測試

生成結果示例

valid for (mins): 1440
Token grants:
{
  "roomJoin": true,
  "room": "my-first-room"
}
Access token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

6. 啟動 LiveKit Server

啟動命令

./bin/livekit-server --keys "356APISejy567Mgg9X7wYzw: 2ll78HjY2MvB2yGSCueswesd28GnuhjGN4c02JuijhclQ"

注意: key 和 secret 之間有一個空格

7. 測試連接

方式一:使用 lk 命令加入房間

lk room join --identity user1 \
--api-key "356APISejy567Mgg9X7wYzw" \
--api-secret "2ll78HjY2MvB2yGSCueswesd28GnuhjGN4c02JuijhclQ" \
--publish-demo my-first-room

方式二:通過 Web 界面測試

  1. 打開網址:https://meet.livekit.io/?tab=custom
  2. 輸入之前生成的 access token
  3. 連接成功後即可打開本地攝像頭
  4. 兩個用戶(user1 和 user2)都執行相同操作,分別填入各自的 access token

8. 測試效果

  • 同一房間內的兩個用戶可以互相看到視頻
  • 支持本地攝像頭開啟
  • 可以進行實時視頻通話

總結

LiveKit 是一個功能強大的開源 WebRTC 服務器,支持房間管理、Redis、信令業務、流媒體 SFU 等功能。通過以上步驟可以快速搭建開發環境並進行測試。

Pytorch

動手學深度學習

動手學深度學習(Pytorch)-線上中文電子書

動手學深度學習(Pytorch)-程式碼

官方教程

Pytorch官方教程-中文

MANNING

Deep Learning with Pytorch-線上原文電子書

Deep Learning with Pytorch-程式碼


那些年我們一起學過的深度學習 系列

Knock Knock! Deep Learning 系列

用免費的GPU+Pytorch訓練自己的分類模型-以智能車位分析為例

PyTorch 學習筆記

colab 使用GPU 的方法

Edit ->NoteBook Settings 選GPU

img

如果要將定義好的張量放到GPU上執行,可以用x.cuda()來指定

import torch
import numpy as np
x_tensor = torch.rand(5,3)
y_numpy = np.random.rand(5,3)
x_numpy = x_tensor.numpy()
y_tensor = torch.from_numpy(y_numpy)

print(x_tensor)
print(x_numpy)
print(y_numpy)
print(y_tensor)

if torch.cuda.is_available():
    x = x_tensor.cuda()
    y = y_tensor.cuda()
    print(x+y)

關於自動微分變數,在使用自動微分變數後,針對後續變數的計算,系統會自動展開計算突來運算。也因為這個關係,可以很快地運用.backward 來執行反向傳播演算法。

下面的例子宣告x 是個張量變數(tensor),無法調用x.grad_fn方法

import torch
import numpy as np
x = torch.ones(3,3)
y = x + 10
print(x)
print(x.grad_fn)

傳回

tensor([[1., 1., 1.], [1., 1., 1.], [1., 1., 1.]])
None

宣告x為自動微分變數, x = Variable(torch.ones(2,2),requires_grad = True)

import torch
import numpy as np
from torch.autograd import Variable
x = Variable(torch.ones(2,2),requires_grad = True)
y = x + 2
print(y.grad_fn)

傳回 <AddBackward0 object at 0x7fc37bd17438>

import torch
import numpy as np
from torch.autograd import Variable
num_x = np.array([[1.0, 2.0],[3.0,4.0]])
tensor_x = torch.from_numpy(num_x)
x = Variable(tensor_x,requires_grad = True)
y = x + 2
z = y*y
print(z)
m = torch.mean(z)
print(m)

傳回

tensor([[ 9., 16.], [25., 36.]], dtype=torch.float64, grad_fn=) tensor(21.5000, dtype=torch.float64, grad_fn=)

import torch
import numpy as np
from torch.autograd import Variable
num_x = np.array([[1.0, 2.0],[3.0,4.0]])
tensor_x = torch.from_numpy(num_x)
x = Variable(tensor_x,requires_grad = True)
y = x + 2
z = y*y   #等價  z=torch.mul(y, y)
m = torch.mean(z)
m.backward()
print(x.grad)

傳回

tensor([[1.5000, 2.0000]

, [2.5000, 3.0000]], dtype=torch.float64)


https://minglunwu.com/notes/2020/20200324.html/

研究所時有花時間去了解Neural Network的概念,但卻一直沒有機會進行實作,最近有機會可以從頭開始學習Pytorch,把學習的過程整理記錄下來,希望想要快速上手Pytorch的人,可以透過這篇文章快速入門!

Tensor的基本使用、格式轉換

建立Tensor

a = [1,2,3]
tensor_a = torch.tensor(a)

Tensor 的維度轉換

tensor_reshape = tensor_a.view(1,-1)

Tensor 中的元素型態轉換

僅需要在tensor之後加上轉換的型態即可。

tensor_a_long = tensor_a.long() # 將tensor_a轉換為long資料型態。
tensor_a_float = tensor_a.float() # 將tensor_a轉換為float資料型態。

與其他常用套件之轉換

tensor_b = torch.from_numpy(np_element) # numpy -> torch
np_a = tensor_a.numpy() # torch -> numpy

常會使用到的Module

通常在 Pytorch 時,常會使用到下列的 Module

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

from torch.utils.data import Dataset, DataLoader
from torch.utils.tensorboard import SummaryWriter

建立一個Network

通常透過 Pytorch 建立一個 Network 時,我們習慣透過定義Python 的 Class 來建構我們的 Network. 這一個Python Class 必須具備下列特性:

  1. 必須繼承 torch.nn.Module,這樣才能使用 Pytorch 內建的各種 function,並且與其他 Pytorch 元件進行互動。

  2. 繼承 torch.nn.Module後,會需要 Override 一些特定的 Method:

    • *init*():

    定義在 Initial 此 Class(Network) 時需要初始化的元件。基本上在 Network 中需要使用到的 Layer、Hyperparameter 都需要在這邊先行定義。

    • forward()

      :

      • 透過 Override forward 這個 Method 來定義此 Network 的 Forward Propagation 方式。
      • 值得注意的是 forward() 在 Override 以後,往後透過可以直接透過 call model 來進行 forward(). (請看下方範例)
class MyOwnNet(nn.Module):
    def __init__(self):
        super(my_own_net, self).__init__() #初始化 nn.Module class

        # 以下按照需求定義Layer.
        self.ln = nn.Linear(768, 384)
        self.ln_2 = nn.Linear(384, 10)
    
    def forward(self, x):
        outputs = self.ln(x)
        outputs = self.ln_2(outputs)

if __name__ == "__main__":
    net = MyOwnNet()
    test = list(range(768))
    test_input = torch.tensor(test)
    outputs = net(test_input) # 直接呼叫instance,將會自動執行forward()的function.

建立訓練資料集

在定義好模型架構後,接著需要處理資料的部分。

當然在提供訓練資料時,我們也可以單純透過迴圈的方式自行提取,但是透過 Pytorch 的資料集,我們不需要再特別去處理「Batch size」或是「Shuffle」的問題。

這裡記錄了兩種 Pytorch 內建的 Module 提供我們實作並且改寫:

  1. Dataset
  2. DataLoader

Dataset

實作時,與上節建立 Network 相同,需要先定義一個 Python Class 來繼承 torch.utils.data.Dataset,並且要Override 以下 Methods:

  • *init*(self): 初始化 instance 時需要進行的動作,通常會在這個地方載入資料集、或是進行前處理。
  • _getitem(self, index)__: 定義使用 idx 去 query 元素時要進行的動作。 (通常直接回傳第 index 筆資料)
  • _len(self)__: 定義使用 len() 去取得 instance 元素數量時要進行的動作。 (通常直接回傳資料筆數)
class OwnDataset(Dataset):
    def __init__(self, file_path):
        super(OwnDataset, self).__init__()
        self.data = pickle.load(open(file_path, "rb")) # 讀取資料
    
    def __len__(self):
        return len(self.data)
    
    def __getitem__(self, idx):
        return {"x": self.data["feature"], "y": self.data["label"]}
    
if __name__ == "__main__":
    dataset = OwnDataset("./data/test.pickle") #Initial an instance.

    print(len(dataset)) # Call __len__()
    a_example = dataset[0] # Call __getitem__()

    feature, label = a_example["x"], a_example["y"]

從範例中可以看到使用 Dataset 的好處在於先行定義好回傳資料的格式、以及如何取用資料。進行訓練時就不需要再重複的撰寫取用資料的程式。

也可以在Dataset中加入一個 type 變數來切換要回傳 training、evaluation、testing set. 並且針對傳入的型態不同來進行資料的Sample。

DataLoader

除此之外,再進行訓練時常會需要動態的調整 batch_size 以及需要打亂資料(Shuffle),如果自行撰寫 Function 的話,常會被 index 搞得昏頭轉向。 有時多一個 idx 就會造成 out of range 的錯誤。

此時如果你有按照上述的格式定義好一個 Dataset,那麼以上任務都不用擔心,我們可以透過 DataLoader 直接處理好。

DataLoader 具有幾個參數:

  • dataset: 放入我們剛剛創建的 OwnDataset Instance.
  • batch_size: 一個 batch 要包含多少資料筆數。
  • shuffle: 是否對資料進行隨機調整。
  • num_workers: 透過 Multi-Process 來加速資料的取用,避免訓練時速度被 IO 給限制。(不建議使用在GPU環境)
  • pin_memory: 在使用 GPU 時,啟用此屬性能提升訓練速度。

有關 num_workers, pin_memory 的探討,建議可以參考官方文件

data_loader = DataLoader(dataset= dataset, batch_size= 4, shuffle= True, num_workers= 2, pin_memory= True)

print(len(data_loader)) #回傳當前共有幾個batch,可以直接用這個數值來作為Step.

data_iter = iter(data_loader)
x, y = data_iter.next() # 透過這種方式來取用資料。

for (x,y) in data_iter: # 也可以透過For loop來取用資料。
    some_train_step(x)

在定義完 Dataset 後,透過 DataLoader 來對資料進行訓練前的處理,接著就能按照需求去取得資料。相當的方便且簡潔。


訓練的Pipeline

個人認為建構 Model 的 Pipeline 大略如下:

  1. 定義 Model.
  2. 定義 Dataset.
  3. 定義 Loss 以及 Optimizer.
  4. 進行訓練.

其中第一點以及第二點請參考本文前段。

定義Loss及Optimizer

torch.nn 以及 torch.nn.Functional 中定義了許多不同的 Loss Function,可以根據需求自行選擇. 以下範例以分類問題的 CrossEntropy 為例:

import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

# Init an instance.
criterion = nn.CrossEntropyLoss() 
# 建立一個optimizer來優化 model 的所有"可訓練參數"
optimizer = optim.Adam(model.parameters(), lr=1e-5)

首先,必須先建立計算 Loss 以及 Optimizer 的 Instance。

在建立 Optimizer 時會需要設定優化的對象,通常會直接放 model.parameters(),代表 model 中所有可訓練的參數。而不同的 Optimizer(SGD, Adam, …) 會有不同的參數要進行設定。

進行訓練

feature, label = data_iter.next() #透過前面提到的iterator取得一個batch的資料。

outputs = model(x) # 將訓練資料送入model中進行forward propagation。

loss = criterion(outputs, y) # 回傳當前Forward結果與真實Label的Loss

optimizer.zero_grad() # 先清空當前的梯度值
loss.backward() # 進行Backward Propagation
optimizer.step() # 針對Backward Propagation所得到的梯度調整參數。

接著直接呼叫 model(x) 如同上節所說,就是直接將 x 送入 model 中進行 Forward Propagation。 得到的結果可以直接與真實 label 送到剛剛建立的 Loss Instance 計算 Loss.

在計算完Loss後,我們就能直接使用 loss.backward() 來取得 Loss 對所有參數的梯度。 在 Pytorch 中,我們只有定義 Forward 的方式,而 Backward Propagation 只需要透過短短一行即可得到。

取得每一個參數的梯度以後,就能呼叫剛剛定義的 optimizer.step() 來進行參數調整。

以上就是一次的訓練迭代: Forward propagation -> 計算Loss -> Backward propagation -> Optimize (根據梯度調整Weight.)

實際訓練時可根據需求來不斷從 Data Iterator 中取得資料,重複上述迭代進行訓練,也因為會有不斷的迭代,所以記得使用 optimizer.zero_grad() 來清空上一次的梯度。

另外,在 Pytorch 中的 Tensor 都會有 requires_grad 的屬性,如果啟用的話會自動追蹤計算圖,方便直接呼叫backward(),如果不希望啟用的話,可以透過下列方法解除:

# Method 1
tensor_nograd = torch.tensor([1,2,3], requires_grad=False)

# Method 2
with torch.no_grad(): # 以下做的事情都不會取得梯度。
    # Your Code.

視覺化工具-TensorBoard

基本使用

在訓練的過程中,我們需要觀察 Loss 或是 Accuracy 來確認訓練的效果,雖然可以透過 Print Log 的方式來顯示,但其實透過有更好的工具能夠協助視覺化。

from torch.utils.tensorboard import SummaryWriter

LOGDIR = "./logs/" # Define 資料要被寫入的位置
writer = SummaryWriter(LOGDIR)

for n_iter in range(100):
    writer.add_scalar('Loss/train', np.random.random(), n_iter)
    writer.add_scalar('Loss/test', np.random.random(), n_iter)
    writer.add_scalar('Accuracy/train', np.random.random(), n_iter)
    writer.add_scalar('Accuracy/test', np.random.random(), n_iter)

也就是在定義了一個 writer 後,可以透過 writer.add_scalar 的方式將想要觀測的值記錄下來,同時可以分門別類的設定標籤、Step 或是 Epoch 數目。 除了add_scalar 外,還有許多如 add_imageadd_graph 的方法可以使用。

寫入資料後,執行 tensorboard 即可在 LocalHost 的瀏覽器觀察結果:

pip install tensorboard
tensorboard --logdir="./logs"

img

圖片來源

Remote Server

另外在進行機器學習時,常常會需要使用到遠端主機的 GPU,紀錄一下如何在 Localhost 查看遠端機器的訓練狀態。 首先還是一樣要在訓練過程中透過 SummaryWriter 將 log 寫入。

接著透過 SSH連線將本地端的一個 Port 與遠端機器的特定 Port 綁定在一起,首先在本地端執行:

ssh -L 16001:127.0.0.1:16001 username@server_ip

透過特定 Port 與遠端主機連線。

接著在遠端主機執行

tensorboard --logdir="./logs" --port=16001

同樣的啟動指令,只是規定要在剛剛設定 Port 上啟動服務。

如此一來就能在自己的主機上查看遠端 Server 的訓練狀況了。


儲存、載入Model

在訓練完模型後,需要將模型儲存下來,方便日後驗證或使用。 Pytorch 提供了兩種儲存方法: 完整模型 以及 State_dict

完整模型:

官方較不推薦這種方式,由於是透過 pickle 的方式進行儲存,很可能會遭遇意料之外的問題。

Save

torch.save(model, PATH)

Load

model = torch.load(PATH)
model.eval()

Pytorch 的 model 中,可以透過 model.train() 以及 model.eval() 來切換不同模式,使用 model.eval() 會將 dropout layer 以及 batch_normalization 切換成驗證模式,避免在 Inference 的過程中造成結果不一致。

State Dict

State Dictionary 則是透過 Python 的 Dictionary 來儲存每一層的內容以及權重。如果要查看的話可以透過model.state_dict() 來取得。

Save

torch.save(model.state_dict(), PATH)

Load

model = MyOwnNet() # 要先建立相同的Class Instance.
model.load_state_dict(torch.load(PATH))
model.eval()

使用GPU

在上述的內容完成後,基本上已經可以建立一個簡易的 Neural Network 了,接下來紀錄如何將快速的將資料從 CPU 訓練切換為 GPU.

首先要先確定自己的 Pytorch 是有安裝到 CUDA 版本。 可以透過下列指令確認:

print(torch.cuda.is_available())

如果是 True 則代表有成功偵測到 GPU,若為 False 則可能是設定錯誤!

接著要建立一個 device 變數:

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

接著就是將自己的 Model 以及需要送進 Model 的 Input 都轉換為 GPU 模式.

model = model.to(device)
x, y = x.to(device), y.to(device)
# 以下正常進行使用

只要加上短短一行指令即可切換為 GPU 模式,此時如果在 print 這些 tensor,可以發現數值不變,但是後面多了一個"cuda:n" 屬性,這就代表 Tensor 已經被送到 GPU 去了。


後記

在學習 Pytorch 的過程中,很多教材都是語法居多,透過實際進行任務的方式教學,但我在過程中對於許多 Component都似懂非懂,現在稍微釐清後,記錄下來,希望如果是想學習 Pytorch 的入門者,看完這篇文章可以瞭解一些基本觀念,在看網路上的 Tutorial 或是 Track 別人的 Code 時,能夠不再霧煞煞~

img

出處 : https://medium.com/@a37708867/pytorch-7b9fe2f5f8ed

PyTorch

Reduce inference costs by 71% and drive scale out using PyTorch, TorchServe, and AWS Inferentia. Pushing the state of…

pytorch.org

img

選擇自己需要而且匹配的版本即可複製指令(黃線)

//版本會隨時間更新所以還是以官網為主
//選擇與論文設計相符的通常比較不會出錯
//最新版本通常不穩定所以選擇較舊的版本
$ conda install pytorch torchvision torchaudio pytorch-cuda=11.6 -c pytorch -c nvidia

不確定自己的CUDA版本怎麼辦? 可以開啟 nvidia 驅動程式或是輸入以下指令。

$ nvidia-smi

img

https://pytorch.org/

CUDA Version 為 11.8

使用 Jupyter notebook來測試

更新套件

更新 Anaconda

$ conda update anaconda

更新 Jupyter notebook

$ conda update jupyter

更新 pip

$ python -m pip install --upgrade pip

檢視 Kernel

查看已建立的 Jupyter Kernel

$ jupyter kernelspec list

建立新 Kernel

//一定要先建立好虛擬環境並進入
$ conda activate pytorch

//下面提供兩種方法可以建立新的 Kernel
//方法一
$ pip install ipykernel
//python -m ipykernel install --user --name <kernel_name> --display-name "<kernel_name>"
$ python -m ipykernel install --user --name pytorch --display-name "pytorch"

//方法二
//可以直接執行建立新的 Kernel
//ipython kernel install --user --name="<kernel_name>" --display-name="<kernel_name>"
$ ipython kernel install --user --name="pytorch" --display-name="pytorch"

img

img

可從上面看出兩個指令皆會下載到相同位置,但方法一需要先使用 pip 下載 ipykernel 的模組。

刪除 Kernel

//jupyter kernelspec remove <kernel_name>
$ jupyter kernelspec remove pytorch

開啟 Jupyter notebook

使用 cd 指令到你想要的資料夾並開啟 Jupyter notebook。

$ jupyter notebook

Jupyter notebook 寫幾行指令測試 Pytorch

.ipynb

是 Jupyter notebook 使用的檔案的副檔名,可以逐行執行指令,並且會將已執行過的指令儲存起來,適合用來 trace 別人的 code ,或學習使用。

建立一個新的檔案

注意要使用剛剛建立的 kernel,才會測試的到,不然剛剛的設定都白做了。

img

點選藍色畫記處

輸入指令

import torch

torch.cuda.is_available()

img

表示 Pytorch 可以正常執行

完成 Pytorch 的下載啦,原則上本篇主要是 Jupyter notebook 的使用,只有少部分篇幅說明下載和測試,花了那麼多的步驟其實是為了以後管理方便,不需要的虛擬環境和 Kernel 可以隨時砍掉重來。

其實這個部分應該會比較少用到,通常設定完久久才會刪,有需要再回來看吧。

遺傳演算法框架 deap 簡介與使用

https://zhuanlan.zhihu.com/p/436438875a

deap框架介紹

目前,有許多可用於遺傳演算法的 Python 框架 —— GAFT,DEAP,Pyevolve 和 PyGMO 等。

其中,deap (Distributed Evolutionary Algorithms in Python) 框架支援使用遺傳演算法以及其他進化計算技術快速開發解決方案,得到了廣泛的應用。deap 提供了各種資料結構和工具,這些資料和工具在實現各種基於遺傳演算法的解決方案時必不可少。

creator模組

creator 模組可以作為元工廠,能夠通過新增新屬性來擴展現有類。

例如,已有一個名為 Employee 的類。使用 creator 工具,可以通過建立 Developer 類來擴展 Employee 類:

import deap import creator
creator.create("Developer",Employee,position="Developer",programmmingLanguage=set)

傳遞給 create() 函數的第一個參數是新類的名稱。第二個參數是要擴展的現有類,接下來是使用其他參數定義新類的屬性。如果為參數分配了一個類(例如 dict 或 set ),它將作為建構函式中初始化的實例屬性新增到新類中。如果參數不是類(例如字串),則將其新增為靜態 (static) 屬性。

因此,建立的 Developer 類將擴展 Employee 類,並將具有一個靜態屬性 position,設定為 Developer,以及一個實例屬性,類型為 set 的 programmingLanguages,該屬性在建構函式中初始化。因此實際上等效於:

class Developer(Employee):
    position = "Developer"
    def __init__(self):
        self.programmmingLanguage = set()

這個新類存在於 creator 模組中,因此需要引用時使用 creator.Developer。

使用 deap 時,creator 模組通常用於建立 Fitness 類以及 Individual 類。

建立適應度類

使用 deap 時,適應度封裝在 Fitness 類中。在 deap 框架中適應度可以有多個組成部分,每個組成部分都有自己的權重(weights)。這些權重的組合定義了適合給定問題的行為或策略。

定義適應度策略

為了快速定義適應度策略,deap 使用了抽象 base.Fitness 類,其中包含 weights 元組,以定義策略並使類可用。可以通過使用 creator 建立基礎 Fitness 類的擴展來完成,類似於建立 Developer 類:

creator.create("FitnessMax",base.Fitness,weights=(1.0,))

上述程式碼將產生一個 creator.FitnessMax 類,該類擴展了 base.Fitness 類,並將 weights 類屬性初始化為 (1.0,)值。需要注意的是:weights 參數是一個元組。

FitnessMax 類的策略是在遺傳演算法過程中最大化單目標解的適應度值。相反,如果有一個單目標問題,需要使適應度值最小的解,則可以使用以下定義來建立最小化策略:

creator.create("FitnessMin",base.Fitness,weights=(-1.0,))

還可以定義具有最佳化多個目標且重要性不同的策略:

creator.create("FitnessCompound",base.Fitness,weights=(1.0,0.2,-0.5))

這將產生一個 creator.FitnessCompound 類,它擁有三個不同的適應度組成部分。第一部分權重為 1.0,第二部分權重為 0.2,第三部分權重為 -0.5。這將傾向於使第一和第二部分(或目標)最大化,而使第三部分(或目標)最小化。

適應度儲存方式

雖然權重元組定義了適應度策略,但是一個對應的元組(稱為 values )用於將適應度值儲存在 base.Fitness 類中。這些值是從單獨定義的函數(通常稱為 evaluate() )獲得的。就像 weights 元組一樣,values 元組儲存每個適應度元件(對象)值。

元組 wvalues 包含通過將 values 元組的每個份量與其 weights 元組的對應份量相乘而獲得的加權值。只要得到了實例的適應度值,就會計算加權值並將其插入 wvalues 中。這些值用於個體之間的適應度的比較操作。

建立個體類

在 deap 中,creator 工具的第二個常見用途是定義構成遺傳演算法種群的個體。遺傳演算法中的個體使用可以由遺傳算子操縱的染色體來表示,通過擴展表示染色體的基類來建立 Individual 類。另外,deap 中的每個個體實例都需要包含其適應度函數作為屬性。

為了滿足這兩個要求,利用 creator 來建立 creator.Individual 類:

creator.create("Individual",list,fitness=creator.FitnessMax)

該程式碼片段具有以下兩個效果:

  • 建立的 Individual 類擴展了 Python 的 list 類,這意味著使用的染色體是列表類型
  • 建立的 Individual 類的每個實例將具有之前建立的 FitnessMax 屬性

Toolbox類

deap 框架提供的第二種高效建立遺傳演算法的機制是 base.Toolbox 類。Toolbox 用作函數(或操作)的容器,能夠通過別名機制和自訂現有函數來建立新的運算子。

假設有一個函數 sumOfTwo():

def sumOfTwo(a,b):
    return a + b

使用 toolbox,可以建立一個新的運算,incrementByFive(),該運算子利用 sumOfTwo() 函數建立:

import base
toolbox = base.Toolbox()
toolbox.register("incrementByFive",sumOfTwo,b=5)

傳遞給 register() 函數的第一個參數是新運算子所需的名稱(或別名),第二個參數是被定製的現有函數。建立完成後,每當呼叫新運算子時,其他參數都會自動傳遞給建立的函數,如:

toolbox.incrementByFive(10)

等效於:

sumOfTwo(10, 5)

這是因為 b 的參數已由 incrementByFive 運算子定義為5。

建立遺傳算子

為了快速建構遺傳流程,可以使用 Toolbox 類定製 tools 模組的現有函數。tools 模組包含許多便捷的函數,這些函數包括選擇、交叉和變異的遺傳算子以及程序的初始化等。

例如,以下程式碼定義了三個別名函數,用作遺傳算子:

from deap import tools
toolbox.register("select", tools.selTournament, tournsize=3)
toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("mutate", tools.mutFlipBit, indpb=0.02)

這三個別名函數的詳細說明:

  1. select 註冊為 tools 函數 selTournament() 的別名,且 tournsize 參數設定為 3。這將建立 toolbox.select 運算子,其為錦標賽規模為 3 的錦標賽選擇算子
  2. mate 註冊為 tools 函數 cxTwoPoint() 的別名,這將建立執行兩點交叉的toolbox.mate算子
  3. mutate 註冊為 tools 函數 mutFlipBit() 的別名,並將 indpb 參數設定為 0.02,這將建立一個翻轉每個特徵的機率為 0.02 的位翻轉突變算子

tools 模組提供了各種遺傳算子的實現,以下列示常用遺傳算子的實現函數。

選擇算子主要包括:

selRoulette()				#輪盤選擇
selStochasticUniversalSampling()	#隨機遍歷採樣(SUS)
selTournament()				#錦標賽選擇

交叉算子主要包括:

cxOnePoint()		#單點交叉
cxUniform()		#均勻交叉
cxOrdered()		#有序交叉
cxPartialyMatched()	#實現部分匹配交叉

突變算子主要包括:

mutFlipBit()	#位翻轉突變
mutGaussian()	#常態分配突變

建立物種

tools 模組的 init.py 檔案包含用於建立和初始化遺傳演算法的函數,其中包括initRepeat(),它接受三個參數:

  1. 要放置結果對象的容器類型
  2. 用於生成將放入容器的對象的函數
  3. 要生成的對象數

如:

#產生含有30個隨機數的列表,這些隨機數介於0和1之間
randomList = tools.initRepeat(list,random.random,30)

此示例中,list 是用作要填充的容器的類型,random.random 是生成器函數,而 30 是呼叫生成器函數以生成填充容器的值的次數。

如果想用 0 或 1 的整數隨機數填充列表,則可以建立一個使用 random.radint() 生成隨機值 0 或 1 的函數,然後將其用作 initRepeat() 的生成器函數:

def zeroOrOne():
    return random.randint(0,1)
randomList = tools.initRepeat(list,zeroOrOne,30)

或者,可以利用 Toolbox:

#建立zeroOrOne運算子,使用參數0、1呼叫random.radint()
toolbox.register("zeroOrOne",random.randint,0,1)
randomList = tools.initRepeat(list,tools.zeroOrOne,30)

計算適應度

雖然 Fitness 類定義了確定其策略(例如最大化或最小化)的適應度權重,但實際的適應度是從單獨定義的函數中獲得的。該適應度計算函數通常使用別名 evalidate 來註冊到 Toolbox 模組中:

def someFitnessCalculationFunction(individual):
    """算給定個體的適應度"""
    return _some_calculation_of_of_the_fitness(individual)
#將evaluate註冊為someFitnessCalculationFunction()的別名
toolbox.register("evaluate",someFitnessCalculationFunction)

遺傳演算法實踐詳解(deap框架初體驗)

OneMax問題介紹

OneMax 問題是一個簡單的最佳化任務,通常作為遺傳演算法的 Hello World

OneMax 任務是尋找給定長度的二進制串,最大化該二進制串中數字的總和。例如,對於長度為 5 的OneMax問題,10010 的數字總和為 2,01110 的數字總和為 3。

顯然,此問題的最優解為每位數字均為1的二進制串。但是對於遺傳演算法而言,其並不具備此知識,因此需要使用其遺傳算子尋找最優解。演算法的目標是在合理的時間內找到最優解,或者至少找到一個近似最優解

遺傳演算法實踐

在進行實踐前,應首先明確遺傳演算法中所用到的要素定義。

  • 選擇染色體 由於 OneMax 問題涉及二進制串,因此每個個體的染色體直接利用代表候選解的二進制串表示是一個自然的選擇。在 Python中,將其實現為僅包含 0/1 整數值的列表。染色體的長度與 OneMax 問題的規模匹配。例如,對於規模為 5 的 OneMax 問題,個體 10010 由列表 [1,0,0,1,0] 表示;
  • 適應度的計算 由於要最大化該二進制串中數字的總和,同時由於每個個體都由 0/1 整數值列表表示,因此適合度可以設計為列表中元素的總和,例如:sum([1,0,0,1,0])= 2;
  • 選擇遺傳算子 選擇遺傳算子並沒有統一的標準,通常可以嘗試幾種選擇方案,找到最適合的方案。其中選擇算子通常可以處理任何染色體類型,但是交叉和突變算子通常需要匹配使用的染色體類型,否則可能會產生無效的染色體:
    • 選擇算子:此處選用錦標賽選擇
    • 交叉算子:此處選用單點交叉
    • 突變算子:此處選用位翻轉突變
  • 設定停止條件 限制繁衍的代際數通常是一個不錯的停止條件,它可以確保演算法不會永遠運行。另外,由於我們知道了 OneMax 問題的最佳解決方案(一個全為 1 的二進制串,也就是說其適應度等於代表個體的列表長度),因此可以將其用作另一個停止條件。但是,需要注意的是,對於現實世界中的多數問題而言,通常不存在這種可以公式化精確定義的先驗知識。

遺傳演算法要素組態

在開始實際的遺傳演算法流程之前,需要根據上述要素的設計利用程式碼實現:

  1. 首先匯入所用包:
from deap import base
from deap import creator
from deap import tools
import random
import matplotlib.pyplot as plt
  1. 接下來,聲明一些常數,這些常數用於設定 OneMax 問題的參數並控制遺傳演算法的行為:
ONE_MAX_LENGTH = 100    #length of bit string to be optimized
POPULATION_SIZE = 200   #number of individuals in population
P_CROSSOVER = 0.9       #probability for crossover
P_MUTATION = 0.1        #probability for mutating an individual
MAX_GENERATION = 50     #max number of generations for stopping condition
  1. 接下來,使用 Toolbox 類建立 zeroOrOne 操作,該操作用於自訂 random.randomint(a,b) 函數。通過將參數 a 和 b 固定為值 0 和 1,當在呼叫此運算時,zeroOrOne 運算子將隨機返回 0 或 1:
toolbox = base.Toolbox()#定義toolbox變數
toolbox.register("zeroOrOne",random.randint,0,1)#註冊zeroOrOne運算
  1. 接下來,需要建立 Fitness 類。由於這裡只有一個目標——最大化數字總和,因此選擇 FitnessMax 策略,使用具有單個正權重的權重元組:
creator.create("FitnessMax",base.Fitness,weights=(1.0,))
  1. 在 deap 中,通常使用 Individual 的類來代表種群中的每個個體。使用 creator 工具建立該類,使用列表作為基類,用於表示個體的染色體。並為該類增加 Fitness 屬性,該屬性初始化為步驟 4 中定義的 FitnessMax 類:
creator.create("Individual", list, fitness=creator.FitnessMax)
  1. 接下來,註冊 individualCreator 操作,該操作建立 Individual 類的實例,並利用步驟 1 中自訂的 zeroOrOne 操作隨機填充 0/1。註冊 individualCreator 操作使用基類 initRepeat 操作,並使用以下參數對基類進行實例化:
  • 將 creator.Individual 類作為放置結果對象的容器類型
  • zeroOrOne 操作是生成對象的函數
  • 常數 ONE_MAX_LENGTH 作為要生成的對象數目

由於 zeroOrOne 運算子生成的對像是 0/1 的隨機數,因此,所得的 individualCreator 運算子將使用 100 個隨機生成的 0 或 1 填充單個實例:

toolbox.register("individualCreator",tools.initRepeat,creator.Individual,toolbox.zeroOrOne,ONE_MAX_LENGTH)
  1. 最後,註冊用於建立個體列表的 populationCreator 操作。該定義使用帶有以下參數的 initRepeat 基類操作:
  • 將列表類作為容器類型
  • 用於生成列表中對象的函數 —— personalCreator 運算子

這裡沒有傳入 initRepeat 的最後一個參數——要生成的對象數量。這意味著,當使用 populationCreator 操作時,需要指定此參數用於確定建立的個體數:

toolbox.register("populationCreator",tools.initRepeat,list,toolbox.individualCreator)
  1. 為了簡化適應度(在 deap 中稱為 evaluation )的計算,首先定義一個獨立的函數,該函數接受 Individual 類的實例並返回其適應度。

這裡定義 oneMaxFitness 函數,用於計算個體中 1 的數量。

def oneMaxFitness(individual):
    return sum(individual),#deap中的適用度表示為元組,因此,當返回單個值時,需要用逗號將其聲明為元組
  1. 接下來,將 evaluate 運算子定義為 oneMaxfitness() 函數的別名。使用 evaluate 別名來計算適應度是 deap 的一種約定:
toolbox.register("evaluate",oneMaxFitness)
  1. 遺傳算子通常是通過對 tools 模組中的現有函數進行別名命名,並根據需要設定參數值來建立的。根據上節設計的要素建立遺傳算子:
toolbox.register("select",tools.selTournament,tournsize=3)
toolbox.register("mate",tools.cxOnePoint)
# mutFlipBit函數遍歷個體的所有特徵,並且對於每個特徵值,
# 都將使用indpb參數值作為翻轉(應用not運算子)該特徵值的機率。
# 該值與突變機率無關,後者由P_MUTATION常數設定。
# 突變機率用於確定是否為種群中的給定個體呼叫mutFlipBit函數
toolbox.register("mutate",tools.mutFlipBit,indpb=1.0/ONE_MAX_LENGTH)

遺傳演算法解的進化

遺傳流程如以下步驟所示:

  1. 通過使用之前定義的 populationCreator 操作建立初始種群,並以 POPULATION_SIZE 常數作為該操作的參數。並初始化 generationCounter 變數,用於判斷代際數:
population = toolbox.populationCreator(n=POPULATION_SIZE)
generationCounter = 0
  1. 為了計算初始種群中每個個體的適應度,使用 map() 函數將 evaluate 操作應用於種群中的每個個體。由於 evaluate 操作是 oneMaxFitness() 函數的別名,因此,迭代的結果由每個個體的計算出的適應度元組組成。 得到結果後將其轉換為元組列表:
fitnessValues = list(map(toolbox.evaluate,population)
  1. 由於 fitnessValues 的項分別與 population (個體列表)中的項匹配,因此可以使用 zip() 函數將它們組合併為每個個體分配相應的適應度元組:
for individual,fitnessValue in zip(population,fitnessValues):
	individual.fitness.values = fitnessValue
  1. 接下來,由於適應度元組僅有一個值,因此從每個個體的適應度中提取第一個值以獲取統計資料:
fitnessValues = [indivalual.fitness.values[0] for individual in population]
  1. 統計種群每一代的最大適應度和平均適應度。建立兩個列表用於儲存統計值:
maxFitnessValues = []
meanFitnessValues = []
  1. 遺傳流程的主要準備工作已經完成,在循環時還需設定停止條件,通過限制代際數來設定一個停止條件,而通過檢測是否達到了最佳解(所有二進制串位都為 1 )作為另一個停止條件:
while max(fitnessValues)<ONE_MAX_LENGTH and generationCounter<MAX_GENERATIONS:
  1. 接下來更新代際計數器:
	generationCounter = generationCounter + 1
  1. 遺傳演算法的核心是遺傳運算子。第一個是 selection 運算子,使用先前利用 toolbox.select 定義的錦標賽選擇。由於我們已經在定義運算子時設定了錦標賽大小,因此只需要將物種及其長度作為參數傳遞給選擇運算子:
	offspring = toolbox.select(population,len(population))
  1. 被選擇的個體被賦值給 offspring 變數,接下來將其克隆,以便我們可以應用遺傳算子而不影響原始種群:

這裡需要注意的是:儘管被選擇的個體被命名為 offspring,但它們仍然是前一代的個體的克隆,我們仍然需要使用 crossover 運算子將它們配對以建立實際的後代。

	offspring = list(map(toolbox.clone,offspring)
  1. 下一個遺傳算子是交叉。已經在上節中定義為 toolbox.mate 運算子,並且其僅僅是單點交叉的別名。使用 Python 切片將 offspring 列表中的每個偶數索引項與奇數索引項對作為雙親。然後,以 P_CROSSOVER 常數設定的交叉機率進行交叉。這將決定這對個體是會交叉或保持不變。最後,刪除後代的適應度值,因為它們現有的適應度已經不再有效:
	for child1,child2 in zip(offspring[::2],offspring[1::2]):
		if random.random() < P_CROSSOVER:
			toolbox.mate(child1,child2)
			del child1.fitness.values
			del child2.fitness.values
  1. 最後一個遺傳運算子是突變,先前已註冊為 toolbox.mutate 運算子,並設定為翻轉位突變操作。遍歷所有後代項,將以由突變機率常數 P_MUTATION 設定的機率應用變異算子。如果個體發生突變,我們確保刪除其適應性值。由於該值可能繼承自上一代,並且在突變後不再正確,需要重新計算:
	for mutant in offspring:
		if random.random() < P_MUTATION:
			toolbox.mutate(mutant)
			del mutant.fitness.values
  1. 沒有交叉或變異的個體保持不變,因此,它們的現有適應度值(已在上一代中計算出)就無需再次計算。其餘個體的適應度值為空。使用 Fitness 類的 valid 屬性尋找這些新個體,然後以與原始適應性值計算相同的方式為其計算新適應性:
	freshIndividuals = [ind for ind in offspring if not ind.fitness.valid]
	freshFitnessValues = list(map(toolbox.evaluate,freshIndividuals))
	for individual,fitnessValue in zip(freshIndividuals,freshFitnessValues):
		individual.fitness.values = fitnessValue
  1. 遺傳算子全部完成後,就可以使用新的種群取代舊的種群了:
	population[:] = offspring
  1. 在繼續下一輪循環之前,將使用與上述相同的方法統計當前的適應度值以更新統計資訊:
	fitnessValues = [ind.fitness.values[0] for ind in population]
  1. 獲得最大和平均適應度值,將它們的值新增到統計列表中:
	maxFitness = max(fitnessValues)
	meanFitness = sum(fitnessValues) / len(population)
	maxFitnessValues.append(maxFItness)
	meanFItnessValues.append(meanFItness)
	print("- Generation {}: Max Fitness = {}, Avg Fitness = {}".format(generationCounter,
	maxFitness, meanFitness)
  1. 此外,使用得到的最大適應度值來找到最佳個體的索引,並列印出該個體:
	best_index = fitnessValues.index(max(fitnessValues))
	print("Best Individual = ", *population[best_index], "\n")
  1. 滿足停止條件並且遺傳演算法流程結束後,可以使用獲取的統計資訊,使用matplotlib庫可視化演算法執行過程中的統計資訊,展示各代個體的最佳和平均適應度值的變化:
plt.plot(maxFitnessValues,color='red')
plt.plot(meanFitnessValues,color='green')
plt.xlabel('Generation')
plt.ylabel('Max / Average Fitness')
plt.title('Max and Average fitness over Generation')
plt.show()

該部分完整程式碼如下:

def main():
    population = toolbox.populationCreator(n=POPULATION_SIZE)
    generationCounter = 0
    fitnessValues = list(map(toolbox.evaluate,population))

    for individual,fitnessValue in zip(population,fitnessValues):
        individual.fitness.values = fitnessValue

    fitnessValues = [individual.fitness.values[0] for individual in population]

    maxFitnessValues = []
    meanFitnessValues = []

    while max(fitnessValues) < ONE_MAX_LENGTH and generationCounter < MAX_GENERATION:
        generationCounter = generationCounter + 1
        
        offspring = toolbox.select(population,len(population))
        offspring = list(map(toolbox.clone,offspring))

        for child1,child2 in zip(offspring[::2],offspring[1::2]):
            if random.random() < P_CROSSOVER:
                toolbox.mate(child1,child2)
                del child1.fitness.values
                del child2.fitness.values
        
        for mutant in offspring:
            if random.random() < P_MUTATION:
                toolbox.mutate(mutant)
                del mutant.fitness.values
        
        freshIndividuals = [ind for ind in offspring if not ind.fitness.valid]
        freshFitnessValues = list(map(toolbox.evaluate,freshIndividuals))
        for individual,fitnessValue in zip(freshIndividuals,freshFitnessValues):
            individual.fitness.values = fitnessValue
        
        population[:] = offspring

        fitnessValues = [ind.fitness.values[0] for ind in population]

        maxFitnessValue = max(fitnessValues)
        meanFitnessValue = sum(fitnessValues) / len(population)
        maxFitnessValues.append(maxFitnessValue)
        meanFitnessValues.append(meanFitnessValue)
        print("- Generation {}: Max Fitness = {}, Avg Fitness = {}".format(generationCounter,maxFitnessValue,meanFitnessValue))

        best_index = fitnessValues.index(max(fitnessValues))
        print("Best Indivadual = ", *population[best_index],"\n")

    plt.plot(maxFitnessValues,color="red")
    plt.plot(meanFitnessValues,color="green")
    plt.xlabel("Generation")
    plt.ylabel("Max / Average Fitness")
    plt.title("Max and Average fitness over Generation")
    plt.show()

至此可以開始測試我們的遺傳演算法了,運行程式碼以驗證其是否找到了 OneMax 問題的最優解。

演算法運行

if __name__ == "__main__":
    main()

運行程序時,可以看到程式執行輸出:

- Generation 27: Max Fitness = 99.0, Avg Fitness = 96.805
Best Indivadual =  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 

- Generation 28: Max Fitness = 99.0, Avg Fitness = 97.235
Best Indivadual =  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 

- Generation 29: Max Fitness = 99.0, Avg Fitness = 97.625
Best Indivadual =  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 

- Generation 30: Max Fitness = 100.0, Avg Fitness = 98.1
Best Indivadual =  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 

可以看到 30 代後演算法找到全 1 解,結果適應度為 100,並停止了遺傳流程。平均適應度開始時僅為 53 左右,結束時接近 100。

繪製圖形如下所示:

img

適應度變化

該圖說明瞭最大適應度與平均適應度是如何隨著代數的增加而逐步增加。

CUDF

https://pytorch.org/

nvidia-smi 


Tue Jun  4 00:12:58 2024       
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.171.04             Driver Version: 535.171.04   CUDA Version: 12.2     |
|-----------------------------------------+----------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |         Memory-Usage | GPU-Util  Compute M. |
|                                         |                      |               MIG M. |
|=========================================+======================+======================|
|   0  NVIDIA GeForce RTX 3060        Off | 00000000:01:00.0  On |                  N/A |
|  0%   58C    P8              18W / 170W |   2476MiB / 12288MiB |      0%      Default |
|                                         |                      |                  N/A |
+-----------------------------------------+----------------------+----------------------+
                                                                                         
+---------------------------------------------------------------------------------------+
| Processes:                                                                            |
|  GPU   GI   CI        PID   Type   Process name                            GPU Memory |
|        ID   ID                                                             Usage      |
|=======================================================================================|
|    0   N/A  N/A      2592      G   /usr/lib/xorg/Xorg                         1377MiB |
|    0   N/A  N/A      2867      G   /usr/bin/gnome-shell                         82MiB |
|    0   N/A  N/A      4678      G   /usr/libexec/xdg-desktop-portal-gnome        22MiB |
|    0   N/A  N/A     47979      G   /usr/bin/nautilus                           492MiB |
|    0   N/A  N/A     57448      G   ...AAAAAAAACAAAAAAAAAA= --shared-files       80MiB |
|    0   N/A  N/A    315568      G   /usr/bin/yelp                                 2MiB |
|    0   N/A  N/A    508948      G   ...seed-version=20240531-130126.993000      288MiB |
|    0   N/A  N/A    513981      G   /usr/bin/gnome-text-editor                   12MiB |
|    0   N/A  N/A    969401      G   ...irefox/4336/usr/lib/firefox/firefox       19MiB |
|    0   N/A  N/A   1030653      G   ...ures=SpareRendererForSitePerProcess       73MiB |
+---------------------------------------------------------------------------------------+
conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia
pip install --extra-index-url=https://pypi.nvidia.com cudf-cu12
import cudf

tips_df = cudf.read_csv("https://github.com/plotly/datasets/raw/master/tips.csv")
tips_df["tip_percentage"] = tips_df["tip"] / tips_df["total_bill"] * 100
#
# display average tip by dining party size
print(tips_df.groupby("size").tip_percentage.mean())

初探基因演算法-用 OneMax 問題示範

https://r23456999.medium.com/%E5%88%9D%E6%8E%A2%E5%9F%BA%E5%9B%A0%E6%BC%94%E7%AE%97%E6%B3%95-%E7%94%A8-onemax-%E5%95%8F%E9%A1%8C%E7%A4%BA%E7%AF%84-3c0edc973da8

這次熟悉看看所謂的 Genetic Algorithm; GA 是什麼吧

摘要

本文將介紹基因演算法的重要元素,以及計算過程,最後使用 DEAP 框架實作一次 OneMax 問題,來看看實務上要如何將基因演算法落地。

介紹基因演算法

基因示意圖

基因示意圖,Photo by Sangharsh Lohakare on Unsplash

什麼是基因演算法?

基因演算法是種受到大自然演化方式啟發的一種用於找最佳解的搜尋演算法。藉由模仿大自然演化過程,不斷的選擇伴侶、繁殖,進而迭代出具備最適應的解決方法。同一時間,能夠克服傳統搜尋演算法上遇到的限制,特別是需要具備大量參數和複雜數學才能表達的問題。

物競天擇 — 適者生存

既然基因演算法是仿造大自然演化方式,在這方面最廣為人的理論就是達爾文-物競天擇說,在物競天擇中核心重點為適者生存。

生物要如何做到適者生存呢?

淘汰掉不適應者就行。物種在進行交配後,會藉由交換染色體,外加一點點基因突變,進而生下子代,然後看看這個子代跟現今環境匹不匹配,如果適應不良就自然死去。這邊講的是最適合環境的活下來,不代表活下來的物種就很萬能,例如說:我們把無尾熊直接從澳洲搬運到臺灣居住好了,然後給吃臺灣原生種植物,無尾熊肯定會水土不服,直接陣亡,畢竟這個物種就不是在臺灣生長下來的。再換個說法好了,把一個很內向的人拉到派對場閤中,還要一直與他人互動,肯定會出狀況。

基因演算法重要元素

每個生物都是由最小單位基因 (Gene) 構建成的染色體 (Chromosome)所組合而成,同一個世代當中的染色體則稱為族群(Population)。舉個實際例子說明:假設將英文字母 A-Z 視為基因,一個染色體需要五個基因組成,則可以得到 A-P-P-L-E 作為一個染色體,假設一個族群需要四個染色體組成,則我們可以說 A-P-P-L-E、A-P-P-L-A、A-P-P-L-I、A-P-P-L-O 視為一個族群。

img

圖片取自:Genetic Algorithm: Reviews, Implementations, and Applications

既然都有染色體了,不免俗就要讓染色體之間開始交配(Crossover)下,開始產出下一代,過程上,必須要從既有族群中選擇(Selection)出要兩兩交配的染色體,拿剛剛的英文字母染色體說明,我們從族群中抓出 A-P-P-L-I、A-P-P-L-O 兩個染色體做交配,交配時,每個染色體各貢獻一半基因,A-P-P-L-I出 A-P-L ,A-P-P-L-O 出 P-O ,組合再一起後得到新染色體 A-P-L-P-O。如果說族群僅有交換基因而已,其變化性是有上限的,所以交配後會隨機的改變染色體當中的基因,這個過程我們稱為突變(Mutation),例如 A-P-L-P-O 變成 B-P-L-P-O 如此一來就得到一個全新的染色體。至於染色體是不是能夠好好的活在世界上,就需要評估染色體的生存能力,這要看**適應度(Fitness)**如何了。

Genetic-operators-selection-crossover-and-mutation

圖片取自:Power of heterogeneous computing as a vehicle for implementing E3 medical decision support systems

重要元素整理

  • 基因(Gene):GA 演算法中最小單位。
  • 染色體(Chromosome):由多個基因組合而成。
  • 族群(Population):由多個染色體組合而成。
  • 交配(Crossover):藉由兩兩染色體部分基因組合成新的染色體過程。
  • 突變(Mutation):交配後,組合新染色體時,對部分基因進行機率性變化。
  • 適應度(Fitness):一組染色體在環境上的合適分數。

基因演算法計算過程

將上述過程整理到基因演算法上時,可以得到以下流程圖

img

圖片取自 Big-Data And Business Intelligence

基因演算法過程,我們將依照以下順序說明

  • 初始化族群(Population Initialization)
  • 定義適應度計算方式(Fitness Assignment)
  • 應用選擇階段(Selection)、交配階段(Crossover)、突變階段(Mutation)

初始化族群(Population Initialization) 初始化族群將隨機建立染色體建立族群中第一個世代。建立的染色體需符合我們為要解決問題的染色體格式,例如說固定長度的 Binary String List。實務上,需要定義族群大小(Population size)。

定義適應度計算方式(Fitness Assignment) 替每個染色體計算適應度函數(Fitness Function)結果。程式中,由於每個染色體的適應度皆是各自獨立的,因此可以同時進行此計算。由於適應度計算之後的選擇階段通常認為具有較高適應度分數的個體是更好的解決方案,因此基因演算法會自然地傾向於尋找適應度函數的最大值。

應用選擇階段(Selection)、交配階段(Crossover)、突變階段(Mutation) 選擇階段負責從當前族群中選擇染色體,以取得最大優勢。 交配階段負責將兩個選定的染色體,交換部分基因,以此來創建新染色體。 突變階段負責將新染色體部分基因進行機率性調整。

每個階段在基因演算法(GA)中各自都有多種進行方式,例如選擇階段中就有常見的輪盤選擇法(Roulette Wheel Selection)比賽選擇法(Tournament selection)等,在交配階段就會有單點交配(Single-point crossover)多點交配( k-point crossover)等,突變階段則會有翻轉位元突變(Flip bit mutation)交換突變(Swap Mutation)等。為了版面乾淨,日後我們再開一篇專門替各個階段中的方法進行說明。

基因演算法應用場景

以下是幾種常用基因演算法的場景:

  1. 旅行推銷員問題
  2. 最佳化神經網路訓練
  3. 生產排程規劃
  4. 大學排課問題
  5. 電路最佳化設計

其實不難發現,都是日常生活中不容易解決的情境需求,而這些問題往往都會有很多種變因存在,例如生產排程規劃來說,這問題講的是一個工廠如何安排多名工人與多臺機器在一個禮拜中可以最大化運作時間,安排上就會需要考慮,每一臺機器的工作時間是不是會相衝,工人的上班時間是不是會跟機器可以工作時間對不上,計算過程就需要慢慢條列出手上有的資源、限制,以及評價最好的狀況等,而以下場景,都有這種特性。

實作基因演算法

這段落中,我們將利用 DEAP (Distributed Evolutionary Algorithms in Python) 框架,示範目前為止基因演算法(GA)當中的觀念是如何落地的。這次我們將使用 OneMax 問題做示範。

OneMax 問題介紹

Jㄍ問題有點像是最佳化世界中的 Hello World,如果是用 Big Data 舉例的話,類似於 Word Count,這樣類比應該可以體會吧XD。

OneMax 問題是說如何讓一段長度固定的二進位字串所有位置上數字之和最大。讓我們用一個長度為5的二進位字串為例:

  • 10010 -> 和為2
  • 00111 -> 和為3
  • 11111 -> 和為5(最大值)

顯然地,當所有數字皆為1時,該字串和最大,但電腦不知道這件事情,需要靠演算法慢慢把答案推敲出來,讓我們來看看 DEAP 怎麼實現過程。

解法思路

首先,該問題是要計算一段長度固定的二進位字串,為此我們可以設定固定長度為 100 先,長度為 100 的二進位字串,其實這就是我們要追求的解,也就是最適應問題的一組染色體。顯而易見的我們的染色體就是長度為 100 的二進位字串,當中的基因只有兩個 0、1。最最最重要的適應度函數顯然就是計算二進位字串中有幾個 1 。選擇階段使用輪盤選擇法、交配階段選擇單點交配、突變階段選擇位元翻轉突變法。

程式實作

先行導入需要的工具

## 導入需要用到的工具
from deap import base, creator, tools, algorithms
import random
import numpy
import matplotlib.pyplot as plt
import seaborn as sns

設定固定參數

# problem constants:
ONE_MAX_LENGTH = 100  # length of bit string to be optimized# Genetic Algorithm constants:
POPULATION_SIZE = 100
P_CROSSOVER = 0.9  # probability for crossover
P_MUTATION = 0.1   # probability for mutating an individual
MAX_GENERATIONS = 50##by defining a constant for the number of individuals we want to keep in the hall of fame.
HALL_OF_FAME_SIZE = 10# set the random seed:
RANDOM_SEED = 42
random.seed(RANDOM_SEED)

設定 DEAP 中的工具

# initialize toolbox
toolbox = base.Toolbox()# create an operator that randomly returns 0 or 1:
toolbox.register("zeroOrOne", random.randint, 0, 1)# define a single objective, maximizing fitness strategy:
creator.create("FitnessMax", base.Fitness, weights=(1.0,))# create the Individual class based on list:
creator.create("Individual", list, fitness=creator.FitnessMax)# create the individual operator to fill up an Individual instance:
toolbox.register("individualCreator", tools.initRepeat, creator.Individual, toolbox.zeroOrOne, ONE_MAX_LENGTH)# create the population operator to generate a list of individuals:
toolbox.register("populationCreator", tools.initRepeat, list, toolbox.individualCreator)

定義適應度函數

# fitness calculation:
# compute the number of '1's in the individualdef oneMaxFitness(individual):
  return sum(individual),  # return a tupletoolbox.register("evaluate", oneMaxFitness)

定義選擇階段、交配階段、突變階段

# genetic operators:mutFlipBit
# Tournament selection with tournament size of 3:
toolbox.register("select", tools.selTournament, tournsize=3)# Single-point crossover:
toolbox.register("mate", tools.cxOnePoint)# Flip-bit mutation:
# indpb: Independent probability for each attribute to be flippedtoolbox.register("mutate", tools.mutFlipBit, indpb=1.0/ONE_MAX_LENGTH)

定義執行階段邏輯

# Genetic Algorithm flow:def main():
  # create initial population (generation 0):
  population = toolbox.populationCreator(n=POPULATION_SIZE)
  
  # prepare the statistics object:
  stats = tools.Statistics(lambda ind: ind.fitness.values)
  stats.register("max", numpy.max)
  stats.register("avg", numpy.mean)
  
  # define the hall-of-fame object:
  hof = tools.HallOfFame(HALL_OF_FAME_SIZE)  # perform the Genetic Algorithm flow:
  population, logbook = algorithms.eaSimple(
    population,toolbox,cxpb=P_CROSSOVER,mutpb=P_MUTATION,
    ngen=MAX_GENERATIONS,stats=stats,halloffame=hof,verbose=False)  # print Hall of Fame info:
  print("Hall of Fame Individuals = ", *hof.items, sep="\n")
  print("Best Ever Individual = ", hof.items[0])  # Genetic Algorithm is done - extract statistics:
  maxFitnessValues, meanFitnessValues = logbook.select("max", "avg")  # plot statistics:
  sns.set_style("whitegrid")
  plt.plot(maxFitnessValues, color='red')
  plt.plot(meanFitnessValues, color='green')
  plt.xlabel('Generation')
  plt.ylabel('Max / Average Fitness')
  plt.title('Max and Average Fitness over Generations')
  plt.show()if __name__ == "__main__":
  main()

結果圖

沒意外的話,會得到類似下方的結果圖,圖中顯示歷經 50 個世代下,族群當中的染色體們所能達到最好的分數,以及平均分數。就結果上可以看出,要到三十幾次的世代結果才能夠找到長度為 100 的二進位字串最大值,也又是全部都是1的這個結果。若你已經曉得上述當中的參數用意的話,可以試著改變 POPULATION_SIZE、P_MUTATION 等,來看看計算變化。

img

Max and Average Fitness Over Generations

結語

希望至此,對於基因演算法算是有初步認識,因為這些元素會一直一直出現,我覺得難的還是針對問題找到合適的染色體編碼方式,接者在定出適應度函數,下一回我們再來看看比較複雜的問題下,要如何思考與實作。有必要介紹 DEAP 工具使用的話,我會再另行開一篇進行說明。

簡單解釋梯度下降法 (Gradient Descent)

https://medium.com/@arlen.mg.lu/%E6%B7%B1%E5%BA%A6%E5%AD%B8%E7%BF%92%E8%AC%9B%E4%B8%AD%E6%96%87-gradient-descent-b2a658815c72

首先,以下的圖先對李宏毅大師致敬

李宏毅大師已經有對Gradient Descent有詳盡的解釋:影片連結

img

如果上述的圖跟李宏毅大師的連結已經有深度理解的人,那就可以不用繼續往下看了,這篇的目的是理解為什麼微分出來的梯度可以幫助找到更小的損失函數,並透過實例的方式來幫助理解 (其實就是給脫離微積分很久的同學看,花哈哈哈)。

正篇開始

Gradient Descent 是深度學習為了找尋接近目標點的一種方法,它是透過一步步慢慢靠近目標的方式,最終找到一個極近似目標的函數。

首先,我們先假設有以下三個點,這三個點是本篇的目標範例,我們想請深度學習程式來找一個函數接近以下三個點。但一開始,程式不是學過數學的人類,並不知道有什麼演算法可以找到一個接近這三個點的函數。

img

三個目標點

於是,程式會先隨機產一條函數,假設是y = 3x + 1好了,取名為函數i(實際上在深度學習中有很多初始化函數的方式)。

img

函數i: y = 3x + 1

好,接下來,我們要怎麼知道函數i有沒有接近三個目標點呢?

很簡單,把三個目標點X值輸入到函數i就會產生函數iY值,對吧?

我們試著輸入X值到函數i後,就會像是如下藍色的點。

img

看起來函數i沒有一個點跟目標點一樣,所以函數i似乎不盡理想,要找一個下一個函數才行。可是,函數i是隨機產生的,要繼續瞎子摸象的來產生函數嗎? 我們應該有更聰明的辦法~!

函數i的結果應該能幫助我們猜測下一個函數吧?

我們先來看看函數i的產出跟三個目標點到底差多遠?

img

函數i 對每個目標點的距離,差值後取平方是為了保證成為正數

所以函數i跟目標點一共差了11的距離,嗯… 那麼也就是說…

我們只要試著一步一步把11的距離縮短了就好吧?

根據這個想法,我們來看看總距離某目標點距離怎麼算的。

總距離 = (每個目標點的Y值 - 函數i對應每個目標點X值的輸出值)²

某目標點距離 = (某目標點的Y值 - 函數i對應某目標點X值的輸出值)²

總距離以及某目標點距離的計算方法是我們假設的一個公式,是為了我們方便理解什麼叫做距離的公式,然而,這個假設的公式也是一個函數,這其實就是所謂的損失函數L。距離目標點越近(數值越小),就表示該函數越靠近三個目標點,也就是損失越少,如果有一個函數可以完全符合三個目標點,那這樣損失函數得到的結果就應該是 0

根據上述,我們將函數i的帶入損失函數L

損失函數L= (y - (3x + 1))² = (y - 3x -1)²

那損失函數L的組成是什麼呢?

img

損失函數L的組成

我們可以觀察到,損失函數L的組成,是來自兩個部分。

  1. 猜的
  2. 目標點的資料

猜的部分是想更改的目標,我們就是期望改了它,可以讓損失函數更靠近0,盡可能的損失越少越好。為了能將變數進行替換,我們將猜的部分換成代數,將3替換成w1替換成b,所以就變成如下 :

img

損失函數L將3替換成w,1替換成b

L = (y - wx - b)² , 這就是我們要處理的損失函數。w跟b會一直變,所以損失函數L也會一直變,但是,目標點是不變的。我們可以試著只帶入一個點,只讓一個點的損失變少,看看這樣會有什麼效果?

我們用目標點 (x = -1 , y = -3)帶入損失函數L

得到損失函數L(x = -3, y = -1) = (-3 +w - b)²

換句話說,只要讓 w - b 接近3得到 -3 + 3 = 0,

那這不就是我們對x = -3, y = -1的目標嗎。

結論A: 損失函數L可以幫助我們找到接近目標的新函數。

不過就算這麼說 ,w 跟 b有無限種組合,而且 w - b 接近3只是滿足 x = -3, y = -1的這點,也不一定能滿足 x = 0, y = 0或是x = 1, y = 1的組合。

那該怎麼做呢? 函數已經有了,我們該怎麼找到建議值呢?

偉大的數學告訴我們,要找到數值是怎麼移動的,我們可以做微分!

微分的意義在於

微分某變數後產生的函數,可以指出原函數每點的變化。

讓我們來看點例子,以函數 y = x³ + x² + x 為範例

img

範例函數:y = x³ + x² + x

接著我們把y = x³ + x² + x 對x作微分,會得到函數y’

y’ = 3x + 2x + 1

好,那我們帶入幾個點來看看數值是怎麼移動的

當x = 1, y=3,y’= 6

當x = 0, y=0,y’= 1

當x = -1, y=-1,y’= -1

當x = -2, y=-6,y’= -6

img

也就是說當x為正數時,y會越來越大,x為負數時,y會越來越小。

x = 0的時候,y’= 1,是一個正數,代表接下來的y是會往正數移動,的確x從0增長到1的時候,y也從0增長到3,y是有越來越大的傾向。

x = -1的時候,y’= -1,是一個負數,代表接下來的y是會往負數移動,的確x從-1增長到-2的時候,y也從-1增長到-6,y是有越來越小的傾向。

因此,我們就得到了數值移動的方向的判定方法: 將x帶入微分後的函數y’,得到的數值如果是正,表示y是往正數移動,得到的數值如果是負,表示y是往負數移動。

通常,我們會將帶入x後的y’產生的數值,稱之為梯度。

結論B : 數值移動的方向是可以從梯度判定的。

回到正題,讓我們回來處理損失函數L(x = -3, y = -1) = (-3 + w - b)²

最終的目的是希望 (-3 +w - b)²越來越小,但是L有兩個變數,w跟b。所以需要分別對w跟b做微分,來得知w跟b怎麼移動才會讓(-3 +w - b)²越來越小。

展開(-3 + w - b)² = 9 - 6w + 6b - 2wb + w² + b²

對w取微分後得到函數L'w = -6 - 2b+ 2w

L’w 可以告訴我們變數w是怎麼移動L函數的

將w = 3, b = 1帶入L’w = -6 - 2b+ 2w= -6 - 2 + 6 = -2,這表示w在(w = 3, b = 1)時,梯度為-2,在此點,w越往負數方向移動損失函數會越小

對b取微分後得到函數L’b : 6 - 2w + 2b

L’b 可以告訴我們變數b是怎麼移動L函數的

將w = 3, b = 1帶入L’b = 6 - 2w + 2b = 6 - 6 + 2 = 2,這表示b在(w = 3, b = 1)時,梯度為2,在此點,b越往正數方向移動損失函數會越大

當知道方向後,我們就知道該如何調整數值。

w在3之後,是越減少越會讓損失函數L減少,但我們不能無窮盡的減下去,剛好就好,所以對w應該要增加,與L’w產生的梯度相反。b在1之後,是越增加越會讓損失函數L變大,所以對b要減少數值,與L’b產生的梯度相反。

結論C : 損失函數L移動的方向剛好跟梯度的方向相反。

可是,該把w變多大,該把b變多小呢?

其實這是沒有答案的,因為就是不知道要修正到多少才是對的,我們還是需要一個可調整的參數來搭配,這個數值叫學習率,它的意義在於動態調整移動的步伐,我們用小寫l來代替,先把l設定成0.5。

接著,開始嘗試找下一個函數,這邊取名函數j

因為函數j跟函數i會有關係,所以我們會用函數i的w以及b與其在(w=3, b=1)梯度來做計算,更新的方式如下:

函數j的w = 函數i的w 減去 學習率 * 函數i的w=3在L(x = -3, y = -1)的梯度

函數j的b = 函數i的b 減去 學習率 * 函數i的b=1在L(x = -3, y = -1)的梯度

注意: 這邊的減去,是來自於結論C (損失函數L移動的方向剛好跟梯度的方向相反)

函數j的w = 3 - (0.5) *(-2) = 4

函數j的b= 1 - (0.5) *(2) = 0

所以函數j : y = 4x + 0 = 4x

我們實際把函數j代入每一點看看與目標點的距離

img

函數j 對每個目標點的距離

可以看到函數j函數i 更靠近三個目標點了,這整個架構就是**Gradient Descent。**實際上,程式會不斷的更動損失函數或者學習率,用各種理論企圖找到一個最接近目標點的函數,還有很多技巧跟實作方式,沒有我寫的那麼的簡單,但為了易於理解整個概念,而有了這篇文章的誕生,感謝收看。

結論A: 損失函數L可以幫助我們找到接近目標的新函數。

結論B : 數值移動的方向是可以從梯度判定的。

結論C : 損失函數L移動的方向剛好跟梯度的方向相反。

梯度下降法(gradient descent)

https://chih-sheng-huang821.medium.com/%E6%A9%9F%E5%99%A8%E5%AD%B8%E7%BF%92-%E5%9F%BA%E7%A4%8E%E6%95%B8%E5%AD%B8-%E4%BA%8C-%E6%A2%AF%E5%BA%A6%E4%B8%8B%E9%99%8D%E6%B3%95-gradient-descent-406e1fd001f

微積分找極值方式:

一般微積分說將要找極大值或極小值的式子做微分等於0找解,找到的不是極大值,就是極小值,是極大還是極小就看二階微分帶入找出來的解,看結果是大於0,還是小於0。

這邊舉個範例

img

微分很簡單

img

微分等於0

img

二階微分

img

所以剛剛的式子找到的極值是極小值,當x=5,有極小值-24。

這個範例是可以找的到唯一解的式子,但在實際應用根本不可能向微積分考試這麼理想一定找得到唯一解,這時候就必須要靠找近似解的方式去逼近極值,也就是這篇要說的梯度下降法(gradient descent)。

梯度下降法(gradient descent)

梯度下降法(gradient descent)是最佳化理論裡面的一個一階找最佳解的一種方法,主要是希望用梯度下降法找到函數(剛剛舉例的式子)的局部最小值,因為梯度的方向是走向局部最大的方向,所以在梯度下降法中是往梯度的反方向走。

這邊我們先大概說一下梯度, 要算一個函數f(x)的梯度有一個前提,就是這個函數要是任意可微分函數,這也是深度學習為什麼都要找可微分函數出來當激活函數(activation function)。

一維度的純量x的梯度,通常用f'(x)表示。 多維度的向量***x***的梯度,通常用∇f(*x*)表示。

白話一點,一維度的純量x的梯度就是算f(x)對x的微分,多維度的向量x***的梯度就是算f(x*)對*x***所有元素的偏微分

一維度的容易理解,上面也有範例。多維度的梯度,一般公式寫的是

img

這邊可能有人看不懂,我舉一個實際的例子

假設我們的***x***有兩個維度的參數,梯度就分別需要對不同維度的參數做偏微分

img

多維度的範例1:

img

多維度的範例2:

img

從一開始的純量的微分到多維度的梯度,大家應該知道梯度怎麼算了。

那算出來的梯度跟梯度下降法有什麼關係?

在機器學習,通常有一個損失函數(loss function或稱為cost function,在最佳化理論我們會稱為目標函數objection function),我們通常是希望這個函數越小越好(也就是找極小值),這邊可以參考回歸分析或是MLP描述的目標函數。 雖然回歸有唯一解,但我在回歸最後面有寫到,因為回歸有算反矩陣等,計算複雜度相對梯度下降法來的複雜,而且也有可以因為矩陣奇異,反矩陣推估錯誤,導致模型估計錯誤,所以用梯度下降法來做應該比較合適。

梯度下降法是一種不斷去更新參數(這邊參數用***x***表示)找「解」的方法,所以一定要先隨機產生一組初始參數的「解」,然後根據這組隨機產生的「解」開始算此「解」的梯度方向大小,然後將這個「解」去減去梯度方向,很饒舌,公式如下:

img

這邊的t是第幾次更新參數,γ是學習率(Learning rate)。 梯度的方向我們知道了,但找「解」的時候公式是往梯度的方向更新,一次要更新多少,就是由學習率來控制的,後面會有範例說這個學習率影響的程度。

範例1

我這邊用下面這個函數(雖然它有唯一解)當例子來做梯度下降法,多維度基本上差不多

img

此例子基本上學習率可以不用太小,就可以很快就找到解,我後面有跑不同學習率看幾次可以跑到近似解。

img

f(x)=x²-10x+1

Note: 我這邊列出切線和法線公式,主要是我範例用的圖有畫出這兩條線。

img

剛有提到我們需要先設定一個初始化的「解」,此例我設定x(0)=20(故意跟最佳值有差距)

紅色的點是每一次更新找到的解

紅色線是法線,藍色線是切線,法線和切線這兩條線是垂直的,但因為x軸和y軸scale不一樣,所以看不出來它是垂直的。

img

學習率是0.01

img

學習率是0.1

img

學習率是0.9

img

學習率是1

由上圖我們可以發現學習率對找解影響很大,學習率太低,需要更新很多次才能到最佳解,學習率太高,有可能會造成梯度走不進去局部極值(但也可以擺脫局部極值的問題,等等有範例)。這邊尤其是當學習率是1的時候,基本上梯度下降法根本走不到局部極小值,一直在左右對跳,所以最佳化理論有很多衍生的方式或更先進的方式去解決這些問題(這邊先不介紹)。

範例2

我設計一個有局部極小值和全域極小值的函數,到四次方,但我是亂打的,所以x=10,函數的值就超大的。

img

我們需要先設定一個初始化的「解」,此例我設定x(0)=-20(故意跟最佳值有差距)

img

學習率是0.00001

所以這個學習率太小,初始值不好,解就會掉到局部極小值。

img

學習率是0.0004

這個學習率(0.0004)對此例子來說,雖然步伐夠大跳出了局部極值,但到全域極值時,因為步伐太大,所以走不到最好的值。

img

學習率是0.0003

這個學習率(0.0003)對此例子來說就夠了,可以走到全域極值。

補充說明沒有極值的狀況

雖然說微分可以找極值,但很多函數既無最大值,也無最小值,因為函數的長像彎彎曲曲很多次,有局部極值或鞍部,所以一次維分等於0求得的可能是極值,也可以是相對極值。

上面舉的某一個例子,就發生這種情況

img

這個方程式可以找到極值「解」讓f(x)最小(f(x)=16),但這個值真的是最小嗎? 我找個點隨便帶入

img

這個值比微分的最佳解還要小,所以可以得知微分等於0找到的不一定是最佳解,所以用梯度下降法,可以找到更好的解。

下圖我將上式子畫出來它的坐標跟微分解還有梯度法如何讓解更新。

img

紅色點是微分解,藍色點是梯度法不斷更新找解(學習率設定在0.01,主要是為了讓解跑慢一點,動畫才好看)。

這邊我只跑100次,因為解在無窮大的地方,但可以看到loss值不斷在減少中。

當然還有很多手法(比如牛頓法, momentum或是Adam)可以避免上述問題,或是讓解找的更快,但此篇文章只在說明,梯度下降法是什麼,跟它怎麼運作的,未來有時間可以在將這些補上。

坦白說這篇內容雖然很好寫,但作圖很花時間和腦力的,喜歡這篇的可以多拍幾下手給個獎勵吧。

梯度最佳解相關算法(gradient descent optimization algorithms)

https://chih-sheng-huang821.medium.com/%E6%A9%9F%E5%99%A8%E5%AD%B8%E7%BF%92-%E5%9F%BA%E7%A4%8E%E6%95%B8%E5%AD%B8-%E4%B8%89-%E6%A2%AF%E5%BA%A6%E6%9C%80%E4%BD%B3%E8%A7%A3%E7%9B%B8%E9%97%9C%E7%AE%97%E6%B3%95-gradient-descent-optimization-algorithms-b61ed1478bd7

在神經網路中,不論是哪種網路,最後都是在找層和層之間的關係(參數,也就是層和層之間的權重),而找參數的過程就稱為學習,所以神經網路的目的就是不斷的更新參數,去最小化損失函數的值,然後找到最佳解。

梯度下降法(gradient descent,GD)是一種不斷去更新參數找解的方法,前一篇文章「機器學習-基礎數學(二):梯度下降法(gradient descent)」已經介紹,這邊複習一下,公式如下

img

這邊的t是第幾次更新參數,γ是學習率(Learning rate)。 找「解」的時候公式是往梯度的反方向更新,但一次要更新多少,就是由學習率來控制的。

隨機梯度下降法(Stochastic gradient descent, SGD)

我們一般看深度學習的介紹,最常看到的最佳化名稱稱為「隨機梯度下降法(Stochastic gradient descent, SGD)」(這篇我為了縮短篇幅,Mini-batch SGD我把它歸納到SGD內),我們簡單說一下這個跟GD差在哪邊。

在更新參數的時候 GD我們是一次用全部訓練集的數據去計算損失函數的梯度就更新一次參數。 SGD就是一次跑一個樣本或是小批次(mini-batch)樣本然後算出一次梯度或是小批次梯度的平均後就更新一次,那這個樣本或是小批次的樣本是隨機抽取的,所以才會稱為隨機梯度下降法。

Note: 如果有跑過open source API的都會知道需要設定batch size這件事,這件事就是在設定小批次的樣本數。後續方法幾乎都用mini-batch方式作學習。

SGD缺點

SGD一定有一些缺點,才會有後續的演進,在當下的問題如果學習率太大,容易造成參數更新呈現鋸齒狀的更新,這是很沒有效率的路徑。

我這邊舉個例子來說不同學習率對找解這件事情,參數的變化方式

img

下圖是x1和x2和f(x1,x2)算出來的結果,右圖是x軸和y軸是x1和x2,z軸是f(x1,x2)的值;左圖x軸和y軸是x1和x2,上面的線是等高線。 為了說明後面找「解」更新時是往最小走,所以我畫出下右圖,可以明顯發現這是一個類似碗公的形狀,「解」在正中間。所以我們只看「解」的變化(下左圖),不看f(x1,x2)值。

img

左圖是x軸是x1,y軸是x2,等高線是f(x1,x2)反應出來值的大小,所以越中間f(x1,x2)值越小。右圖就是左圖的立體版,x軸是x1,y軸是x2,z軸是f(x1,x2)。

這邊我還是用SGD的方式找解 initial從[x1,x2]=[-8,5]開始去跑 學習率設定0.9,為了區隔和後續算法的差異,我設的大一點。

img

學習率設定0.9

這個例子就是我剛提到的「在當下的問題如果學習率太大,容易造成參數更新呈現鋸齒狀的更新」。

這邊我將學習率設定為0.5,這樣看起來就可以更快更直接走向最佳解。

img

學習率設定0.5

所以當gradient太大(這邊我在圖用gradient norm表示gradient的大小)的時候,如果學習率過大,這時候很容易讓找解的時候一步跨的太大,第一雖然有可能還是往最佳解走,但會發生在最後幾步跳不進最佳解內,這邊我用學習率等於1來呈現什麼叫跳不進最佳解。

img

學習率設定1

由上結果可以得知最好的找解方式是可以在不同學習的時間點用不同的學習率,當然還有考慮不同的方向。

Momentum

img

這邊的t是第幾次更新參數,γ是學習率(Learning rate),m是momentum項(一般設定為0.9),主要是用在計算參數更新方向前會考慮前一次參數更新的方向(v(t-1)),如果當下梯度方向和歷史參數更新的方向一致,則會增強這個方向的梯度,若當下梯度方向和歷史參數更新的方向不一致,則梯度會衰退。然後每一次對梯度作方向微調。這樣可以增加學習上的穩定性(梯度不更新太快),這樣可以學習的更快,並且有擺脫局部最佳解的能力。

這個現象就像是丟一顆球到碗裡,球會在碗內左右振盪,隨著阻力的慢慢趨向最低點,如下圖

img

雖然此例子SGD跑的比momentum快,但可以發現在複雜一點後momentum會比SGD好。

比如「機器學習-基礎數學(二):梯度下降法(gradient descent)」內的例子。

img

之前的例子GD的學習率設定0.00001,GD會掉到local minimum,但momentum則會跳出。

img

學習率0.00001

Adagrad

SGD和momentum在更新參數時,都是用同一個學習率(γ),Adagrad算法則是在學習過程中對學習率不斷的調整,這種技巧叫做「學習率衰減(Learning rate decay)」。通常在神經網路學習,一開始會用大的學習率,接著在變小的學習率,從上述例子可以發現,大的學習率可以較快走到最佳值或是跳出局部極值,但越後面到要找到極值就需要小的學習率。

Adagrad則是針對每個參數客製化的值,所以Ada這個字跟是Adaptive的簡寫,這邊假設 g_t,i為第t次第i個參數的梯度,

img

SGD更新就是

img

Adagrad則是

img

ε是平滑項,主要避免分母為0的問題,一般設定為1e-7。Gt這邊定義是一個對角矩陣,對角線每一個元素是相對應每一個參數梯度的平方和。這邊很有趣,一般文章都寫說分母是the sum of the squares of the gradients, i.e. θi up to time step t,然後就沒有其他說明瞭。

其實G(t)的算法如下,假設*x***是d維度的參數:

img

所以是一開始第一次到第t次的梯度平方和。

這邊舉個例子來說明Adagrad是怎麼計算的,這邊的ε我先設為0,公式比較不亂

img

如果iteration (t)次數越大,如果Gradient平方和(分母)越大,會讓學習率越來越小,這樣學習率就可以達到隨著時間減少的目的,在接近目標函數的最小值時就不會向上圖例在解的左右跳來跳去。但當Gradient平方和(分母)越小,學習率會越大。但因為每個參數的學習率會不一樣,所以在學習過程中就比較不會卡在Saddle point (最後有圖例)。

Adagrad缺點是在訓練中後段時,有可能因為分母累積越來越大(因為是從第1次梯度到第t次梯度的和)導致梯度趨近於0,如果有設定early stop的,會使得訓練提前結束。

RMSProp

RMSprop是Geoff Hinton 提出未發表的方法,和Adagrad一樣是自適應的方法,但Adagrad的分母是從第1次梯度到第t次梯度的和,所以和可能過大,兒RMSprop則是算對應的平均值,因此可以緩解Adagrad學習率下降過快的問題。

img

E[]在統計上就是取期望值,所以是取g_i²的期望值,白話說就是他的平均數。ρ是過去t-1時間的梯度平均數的權重,一般建議設成0.9。

Adam

Adam全名Adaptive Moment Estimation。剛剛介紹的Momentum是「計算參數更新方向前會考慮前一次參數更新的方向」, RMSprop則是「在學習率上依據梯度的大小對學習率進行加強或是衰減」。

Adam則是兩者合併加強版本(Momentum+RMSprop+各自做偏差的修正)。

img

m*t和v*t分別是梯度的一階動差函數和二階動差函數(非去中心化)。因為m*t和v***t初始設定是全為0的向量,Adam的作者發現算法偏量很容易區近於0,因此他們提出修正項,去消除這些偏量,細節: ADAM: A METHOD FOR STOCHASTIC OPTIMIZATION(equ. 4)。

img

Adam更新的準則:

img

作者有建議預設值β1=0.9, β2=0.999, ε=10^(-8)。

這邊舉一個例子將所有算法都去執行一遍,如下圖:

img

目標函數

img

Reference: 神人大作: http://ruder.io/optimizing-gradient-descent/index.html#adam ADAM: https://arxiv.org/abs/1412.6980

這次的部落格,雖然參考了上述的連結,但是裡麵包含了函數講解 ,可以快速入門,上手。

前沿

筆者最近開始學習如何用DEAP落實進化演算法,本文既是教學,也是學習筆記,希望在幫助自己記憶理解的同時對同樣正在學習的同學能有所幫助。礙於筆者水平有限,又非運籌最佳化科班出身,錯誤難免,請大家多多指正。

關於DEAP

DEAP)是一個進化計算框架,能夠幫助我們快速實現和測試進化演算法。由於它的靈活與強大,目前在Github上已經有2848個star。

DEAP的特性:

各類遺傳演算法 遺傳規劃 進化策略 多目標最佳化 多種群之間的協作與競爭 平行計算 計算過程中設定檢查點 設定基準模組,檢驗演算法能力 支援粒子群演算法、差分進化演算法等 可以簡單的使用 pip install deap 來安裝,本文基於當前的最新版(deap - 1.3.1)

進化演算法簡介

什麼是進化演算法

進化演算法(Evolutionary Algorithms)是一類元啟髮式演算法的統稱。這類演算法借鑑大自然中生物的進化、選擇與淘汰機制,通常先產生一個族群,然後不斷進化與淘汰,最終產生能夠在嚴酷的自然環境中生存的優異個體(也就是有較大適應度函數的可行解)。它具有自組織、自適應的特性,常被用來處理傳統最佳化演算法難以解決的問題。

進化演算法的優缺點

優點:

  • 泛用性強,對連續變數和離散變數都能適用;
  • 不需要導數資訊,因此不要求適應度函數的連續和可微性質(或者說不需要問題內在機理的相關資訊);
  • 可以在解空間內大範圍平行搜尋;
  • 不容易陷入局部最優;
  • 高度平行化,並且容易與其他最佳化方法整合。

缺點:

  • 對於凸最佳化問題,相對基於梯度的最佳化方法(例如梯度下降法,牛頓/擬牛頓法)收斂速度更慢;
  • 進化演算法需要在搜尋空間投放大量個體來搜尋最優解。對於高維問題,由於搜尋空間隨維度指數級膨脹,需要投放的個體數也大幅增長,會導致收斂速度變慢;
  • 設計編碼方式、適應度函數以及變異規則需要大量經驗。

進化演算法的基本元素利用這些元素,我們就可以依照流程圖組成一個進化演算法:

  1. 個體編碼(Individual representation): 將問題的解空間編碼對應到搜尋空間的過程。常用的編碼方式有二值編碼(Binary),格雷編碼(Gray),浮點編碼(Floating-point)等。
  2. 評價(Evaluation): 設定一定的準則評價族群內每個個體的優秀程度。這種優秀程度通常稱為適應度(Fitness)。
  3. 配種選擇(Mating selection): 建立準則從父代中選擇個體參與育種。儘可能選擇精英個體的同時也應當維護種群的多樣性,避免演算法過早陷入局部最優。
  4. 變異(Variation): 變異過程包括一系列受到生物啟發的操作,例如重組(Recombination),突變(mutation)等。通過變異操作,父代的個體編碼以一定方式繼承和重新組合後,形成後代族群。
  5. 環境選擇(Environmental selection): 將父代與子代重組成新的族群。這個過程中育種得到的後代被重新插入到父代種群中,替換父代種群的部分或全體,形成具有與前代相近規模的新族群。
  6. 停止準則(Stopping crieterion): 確定演算法何時停止,通常有兩種情況:演算法已經找到最優解或者演算法已經選入局部最優,不能繼續在解空間內搜尋。

img

用文字表述實現一個簡單進化演算法的過程如下:


Generate the initial population P(0) at random, and set t = 0.
repeat
    Evaluate the fitness of each individual in P(t).
    Select parents from P(t) based on their fitness.
    Mate or Mutate to Selected P(t)
    Obtain population P(t+1) by making variations to parents.
    Set t = t + 1

實現:問題定義、個體編碼與建立初始族群

1.最佳化問題的定義

單目標最佳化:creator.create('FitnessMin', base.Fitness, weights=(-1.0, ))

在建立單目標最佳化問題時,weights用來指示最大化和最小化。此處-1.0即代表問題是一個最小化問題,對於最大化,應將weights改為正數,如1.0。

另外即使是單目標最佳化,weights也需要是一個tuple,以保證單目標和多目標最佳化時資料結構的統一。

對於單目標最佳化問題,weights 的絕對值沒有意義,只要符號選擇正確即可。

多目標最佳化:creator.create('FitnessMulti', base.Fitness, weights=(-1.0, 1.0))

對於多目標最佳化問題,weights用來指示多個最佳化目標之間的相對重要程度以及最大化最小化。如示例中給出的(-1.0, 1.0)代表對第一個目標函數取最小值,對第二個目標函數取最大值。

creator.create('FitnessMin', base.Fitness, weights=(-1.0, ))函數講解:

先看下函數的基本定義:該函數主要是為了創造出一個類

第一個參數:表示類名字

第二個參數:需要繼承的一個類,因為這裡是計算fitness,所以我們繼承了base.fitness

第三個參數:會把接下來的所有參數,整理為該類的屬性(attributes),下面的例子說明

img

函數舉例:create("Foo", list, bar=dict, spam=1) 相當於:

class Foo(list):
    spam = 1

    def __init__(self):

Foo作為類名,並繼承了list,如果參數是類(dict),則在__init__ 函數中初始化該實例,且該實例作為Foo類的一個屬性(attributes)。如func.bar。如果參數不是類,則作為該類的一個靜態屬性**(attributes)**。如,func.spam

img


def create(name, base, **kargs):
    """Creates a new class named *name* inheriting from *base* in the
    :mod:`~deap.creator` module. The new class can have attributes defined by
    the subsequent keyword arguments passed to the function create. If the
    argument is a class (without the parenthesis), the __init__ function is
    called in the initialization of an instance of the new object and the
    returned instance is added as an attribute of the class' instance.
    Otherwise, if the argument is not a class, (for example an :class:`int`),
    it is added as a "static" attribute of the class.
    :param name: The name of the class to create.
    :param base: A base class from which to inherit.
    :param attribute: One or more attributes to add on instantiation of this
                      class, optional.
    The following is used to create a class :class:`Foo` inheriting from the
    standard :class:`list` and having an attribute :attr:`bar` being an empty
    dictionary and a static attribute :attr:`spam` initialized to 1. ::
        create("Foo", list, bar=dict, spam=1)
    This above line is exactly the same as defining in the :mod:`creator`
    module something like the following. ::
        class Foo(list):
            spam = 1
            def __init__(self):
                self.bar = dict()
    The :ref:`creating-types` tutorial gives more examples of the creator
    usage.

2.個體編碼

**實數編碼(Value encoding):**直接用實數對變數進行編碼。優點是不用解碼,基因表達非常簡潔,而且能對應連續區間。但是實數編碼後搜尋區間連續,因此容易陷入局部最優。

實數編碼DEAP實現:

from deap import base, creator, tools
import random
IND_SIZE = 5
creator.create('FitnessMin', base.Fitness, weights=(-1.0,)) #最佳化目標:單變數,求最小值
creator.create('Individual', list, fitness = creator.FitnessMin) #建立Individual類,繼承list
 
toolbox = base.Toolbox()
toolbox.register('Attr_float', random.random)
toolbox.register('Individual', tools.initRepeat, creator.Individual, toolbox.Attr_float, n=IND_SIZE)
 
ind1 = toolbox.Individual()
print(ind1)
 
# 結果:[0.8579615693371493, 0.05774821674048369, 0.8812411734389638, 0.5854279538236896, 0.12908399219828248]

creator.create('Individual', list, fitness = creator.FitnessMin)講解

第一個參數:表示名字 Individual

第二個參數:需要繼承的一個類,這裡creator.Individual() 的對象本身有了和list一樣的功能,可以append 和extend。這裡我們資料的保存方式是list,所以list,也可以是tuple

第三個參數:將fitness 這個參數新增到Individual,作為Individual的一個屬性(attributes)

經過剛剛操作,我們creator.Individual(),裡面具有了兩種類型的實例,一種是list,一種是fitness

第一個例子代表:繼承 list 進行初始化賦值,比如a = list([1,2,3,4,5,6])

img

第二個例子代表:繼承 list 進行append

img

通過這個實例化過程,我們可以發現,creator.Individual(),給我們提供了兩個位置,第一個位置表示繼承了list這個類,第二個位置fitness 在初始化的時候,已經有了具體的參數。從下面的結果中看到creator.Individual()只能接收一個參數。

img

第三個例子代表:

根據a = list([1,2,3,4,5])這個特性,list(object),可以接收一個可迭代實例,包括數值型可迭代對象即序列、字典、集合對應的可迭代對象,同時也不知於此,所以:

creator.Individual()可以接收一個自身類的對象,相當於deepcopy,內容一致,但是記憶體地址不一致

img

img

img

img

***toolbox.register('Attr_float', random.random)*講解

1、觀察****toolbox.register函數****

def register(self, alias, function, *args, **kargs):
    """Register a *function* in the toolbox under the name *alias*. You
    may provide default arguments that will be passed automatically when
    calling the registered function. Fixed arguments can then be overriden
    at function call time.
    :param alias: The name the operator will take in the toolbox. If the
                  alias already exist it will overwrite the the operator
                  already present.
    :param function: The function to which refer the alias.
    :param argument: One or more argument (and keyword argument) to pass
                     automatically to the registered function when called,
                     optional.
    The following code block is an example of how the toolbox is used. ::
        >>> def func(a, b, c=3):
        ...     print(a, b, c)
        ... 
        >>> tools = Toolbox()
        >>> tools.register("myFunc", func, 2, c=4)
        >>> tools.myFunc(3)
        2 3 4

第一個參數:表示別名 (後續函數呼叫用這個別名)

第二個參數:表示一個函數

第三個參數:實參組成元組傳進來

第三個參數:實參組成字典傳進來

2、觀察random.random函數,作為第二個參數,function,由於random.random函數裡面不需要傳參,所以就沒有了後續的內容

***toolbox.register('Individual', tools.initRepeat, creator.Individual, toolbox.Attr_float, n=IND_SIZE)*講解

1、觀察****toolbox.register函數****

上面已經說過了,這裡不再贅述

2、觀察****tools.initRepeat函數****

def initRepeat(container, func, n):
    """Call the function *container* with a generator function corresponding
    to the calling *n* times the function *func*.
    :param container: The type to put in the data from func.
    :param func: The function that will be called n times to fill the
                 container.
    :param n: The number of times to repeat func.
    :returns: An instance of the container filled with data from func.
    This helper function can be used in conjunction with a Toolbox
    to register a generator of filled containers, as individuals or

從函數名可以看出來,他包含了初始化+重複

第一個參數:container 是容器,表示可以把東西放進去的容器,但是我們可以放什麼呢?

第二個參數:func 函數,這個func,是把他的結果填充到 容器中。

第三個參數:表示我們需要實現多少次func這個函數。

這個函數整體表示:我們執行n次func 的結果,填充到container中。

3、觀察****toolbox.register與tools.initRepeat整合在一起****

這個函數裡面一共包含了5個實參,那麼如何劃分呢?裡面是對象,套,對象

首先****toolbox.register****裡面需要三個參數

第一個參數:Individual

第二個參數:tools.initRepeat

第三個參數:(creator.Individual, toolbox.Attr_float)組成元組

第四個參數:{n:IND_SIZE} 組成字典

第三個和第四個參數具體如何分配,請查看連結:4. More Control Flow Tools — Python 2.7.18 documentation

第三個和第四個參數主要的意義是表明了register函數可以接收更多的參數,你來多少,我都可以接收。但是最重要的就是,你後續來的參數,其實都是為register裡面的function服務的,具體來講,後續來的參數creator.Individual, toolbox.Attr_float, n=IND_SIZE,都是為了傳遞給tools.initRepeat裡面的,因為tools.initRepeat(container, func, n)包含了三個參數

其次****tools.initRepeat****

第一個參數:container 是容器,creator.Individual

第二個參數:func 函數,toolbox.Attr_float,也就是裡面的 random.random

第三個參數:n=IND_SIZE,表示實現多少次func函數,也就是上面的 toolbox.Attr_float()

這其實相當於一個函數套娃。

img

從這裡可以觀察到,把toolbox.Attr_float()結果組成了一個列表,然後傳遞到creator.Individual中的預留的位置list中(在2、個體編碼中提到)

二進制編碼(Binary encoding)

在二進制編碼中,用01兩種數字模擬人類染色體中的4中鹼基,用一定長度的01字串來描述變數。其優點在於種群多樣性大,但是需要解碼,而且不連續,容易產生Hamming cliff(例如0111=7, 1000=8,改動了全部的4位數字之後,實際值只變動了1),在接近局部最優位置時,染色體稍有變動,就會使變數產生很大偏移(格雷編碼(Gray coding)能夠克服漢明距離的問題,但是實際問題複雜度較大時,格雷編碼很難精確描述問題)。

變數的二進制編碼:

由於通常情況下,搜尋空間都是實數空間,因此在編碼時,需要建立實數空間到二進制編碼空間的對應。使用二進制不能對實數空間進行連續編碼,但是可以在給定精度下對連續空間進行離散化。

以例子來說明如何對變數進行二進制編碼,假設需要對一個在區間[-2, 2]上的變數進行二進制編碼:

*選擇編碼長度:*在需要6位精度的情況下,我們需要將該區間離散為(2-(-2))*10^6個數。我們至少需要22位二進制數字來滿足我們的精度要求。

設定解碼器:img

以隨機生成一個長度為10的二進制編碼為例,本身DEAP庫中沒有內建的Binary encoding,我們可以藉助Scipy模組中的伯努利分佈來生成一個二進制序列。


from deap import base, creator, tools
from scipy.stats import bernoulli
 
creator.create('FitnessMin', base.Fitness, weights=(-1.0,)) #最佳化目標:單變數,求最小值
creator.create('Individual', list, fitness = creator.FitnessMin) #建立Individual類,繼承list
 
GENE_LENGTH = 10
 
toolbox = base.Toolbox()
toolbox.register('Binary', bernoulli.rvs, 0.5) #註冊一個Binary的alias,指向scipy.stats中的bernoulli.rvs,機率為0.5
toolbox.register('Individual', tools.initRepeat, creator.Individual, toolbox.Binary, n = GENE_LENGTH) #用tools.initRepeat生成長度為GENE_LENGTH的Individual
 
ind1 = toolbox.Individual()
print(ind1)

這裡咱們重點說明toolbox.register('Binary', bernoulli.rvs, 0.5)toolbox.register('Individual', tools.initRepeat, creator.Individual, toolbox.Binary, n = GENE_LENGTH)

toolbox.register('Binary', bernoulli.rvs, 0.5)

伯努利分佈的取值,只要0和1,但是取0或者1的機率由參數決定:程式碼中(bernoulli.rvs, 0.5)表示進行一次伯努利實驗,取0或者1的機率為0.5.根據以下實驗證明:

imgimg

toolbox.register('Individual', tools.initRepeat, creator.Individual, toolbox.Binary, n = GENE_LENGTH)

上面已經說明瞭toolbox.register函數的具體意思。這裡具體說明上述整理的意思:首先tools.initRepeat 裡是一個重複性的過程,具體重複的函數是toolbox.Binary,同時承載這個函數的是容器creator.Individual,重複了n次toolbox.Binary,放入容器creator.Individual中。

最後通過 ind1 = toolbox.Individual() 完成了實例化操作

img

序列編碼(Permutation encoding)

通常在求解順序問題時用到,例如TSP問題。序列編碼中的每個染色體都是一個序列。

同樣的,這裡的random.sampole也可以用np.random.permutation代替。

from deap import base, creator, tools
import random
creator.create("FitnessMin", base.Fitness, weights=(-1.0,))
creator.create("Individual", list, fitness=creator.FitnessMin)
 
IND_SIZE=10
 
toolbox = base.Toolbox()
toolbox.register("Indices", random.sample, range(IND_SIZE), IND_SIZE)
toolbox.register("Individual", tools.initIterate, creator.Individual,toolbox.Indices)
ind1 = toolbox.Individual()
print(ind1)
 
#結果:[0, 1, 5, 8, 2, 3, 6, 7, 9, 4]

觀察函數:random.sample(range(IND_SIZE), IND_SIZE)

img

*tools.initIterate* 裡麵包含了容器和生成器,具體的來講,容器指的是(creator.Individual),生成器指的是(toolbox.Indices -> 具體指的是random.sample(range(IND_SIZE),IND_SIZE))

img

def initIterate(container, generator):
    """Call the function *container* with an iterable as
    its only argument. The iterable must be returned by
    the method or the object *generator*.
    :param container: The type to put in the data from func.
    :param generator: A function returning an iterable (list, tuple, ...),
                      the content of this iterable will fill the container.
    :returns: An instance of the container filled with data from the
              generator.
    This helper function can be used in conjunction with a Toolbox
    to register a generator of filled containers, as individuals or
    population.
        >>> import random
        >>> from functools import partial
        >>> random.seed(42)
        >>> gen_idx = partial(random.sample, list(range(10)), 10)
        >>> initIterate(list, gen_idx)      # doctest: +SKIP
        [1, 0, 4, 9, 6, 5, 8, 2, 3, 7]
    See the :ref:`permutation` and :ref:`arithmetic-expr` tutorials for
    more examples.
    """
    return container(generator())

粒子(Particles)編碼

粒子是一種特殊個體,主要用於粒子群演算法。相比普通的個體,它額外具有速度、速度限制並且能記錄最優位置。


import random
from deap import base, creator, tools
 
creator.create("FitnessMax", base.Fitness, weights=(1.0, 1.0))
creator.create("Particle", list, fitness=creator.FitnessMax, speed=None,
               smin=None, smax=None, best=None)
 
# 自訂的粒子初始化函數
def initParticle(pcls, size, pmin, pmax, smin, smax):
    part = pcls(random.uniform(pmin, pmax) for _ in range(size))
    part.speed = [random.uniform(smin, smax) for _ in range(size)]
    part.smin = smin
    part.smax = smax
    return part
 
toolbox = base.Toolbox()
toolbox.register("Particle", initParticle, creator.Particle, size=2, pmin=-6, pmax=6, smin=-3, smax=3) #為自己編寫的initParticle函數註冊一個alias "Particle",呼叫時生成一個2維粒子,放在容器creator.Particle中,粒子的位置落在(-6,6)中,速度限製為(-3,3)
 
ind1 = toolbox.Particle()
print(ind1)
print(ind1.speed)
print(ind1.smin, ind1.smax)
 
# 結果:[-2.176528549934324, -3.0796558214905]
#[-2.9943676285620104, -0.3222138308543414]
#-3 3
 
print(ind1.fitness.valid)
 
# 結果:False
# 因為當前還沒有計算適應度函數,所以粒子的最優適應度值還是invalid

3.初始族群的建立:

一般族群

這是最常用的族群類型,族群中沒有特別的順序或者子族群。

一般族群的DEAP實現:toolbox.register('population', tools.initRepeat, list, toolbox.individual)

以二進制編碼為例,以下程式碼可以生成由10個長度為5的隨機二進制編碼個體組成的一般族群:

from deap import base, creator, tools
from scipy.stats import bernoulli
 
# 定義問題
creator.create('FitnessMin', base.Fitness, weights=(-1.0,)) # 單目標,最小化
creator.create('Individual', list, fitness = creator.FitnessMin)
 
# 生成個體
GENE_LENGTH = 5
toolbox = base.Toolbox() #實例化一個Toolbox
toolbox.register('Binary', bernoulli.rvs, 0.5)
toolbox.register('Individual', tools.initRepeat, creator.Individual, toolbox.Binary, n=GENE_LENGTH)
 
# 生成初始族群
N_POP = 10
toolbox.register('Population', tools.initRepeat, list, toolbox.Individual)
toolbox.Population(n = N_POP)

同類群(Demes)

同類群即一個族群中包含幾個子族群。在有些演算法中,會使用本地選擇(Local selection)挑選育種個體,這種情況下個體僅與同一鄰域的個體相互作用。

同類群的DEAP實現:

from deap import base, creator, tools
from scipy.stats import bernoulli
 
# 定義問題
creator.create('FitnessMin', base.Fitness, weights=(-1.0,)) # 單目標,最小化
creator.create('Individual', list, fitness = creator.FitnessMin)
 
# 生成個體
GENE_LENGTH = 5
toolbox = base.Toolbox() #實例化一個Toolbox
toolbox.register('Binary', bernoulli.rvs, 0.5)
toolbox.register('Individual', tools.initRepeat, creator.Individual, toolbox.Binary, n=GENE_LENGTH)
 
# 生成初始族群
N_POP = 10
toolbox.register('Population', tools.initRepeat, list, toolbox.Individual)
toolbox.Population(n = N_POP)
 
toolbox.register("deme", tools.initRepeat, list, toolbox.Individual)
# 初始化同類群
DEME_SIZES = (10, 50, 100)
population = [toolbox.deme(n=i) for i in DEME_SIZES]
print(population)

粒子群

粒子群中的所有粒子共享全域最優。在實現時需要額外傳入全域最優位置與全域最優適應度給族群。

deap/examples/pso at 09b2562aad70c6f171ebca2bdac0c30387f5e8f5 · DEAP/deap · GitHub

import operator
import random
 
import numpy
 
from deap import base
from deap import benchmarks
from deap import creator
from deap import tools
 
creator.create("FitnessMax", base.Fitness, weights=(1.0,))
creator.create("Particle", list, fitness=creator.FitnessMax, speed=list, 
    smin=None, smax=None, best=None)
 
def generate(size, pmin, pmax, smin, smax):
    part = creator.Particle(random.uniform(pmin, pmax) for _ in range(size)) 
    part.speed = [random.uniform(smin, smax) for _ in range(size)]
    part.smin = smin
    part.smax = smax
    return part
 
toolbox = base.Toolbox()
toolbox.register("particle", generate, size=2, pmin=-6, pmax=6, smin=-3, smax=3)
toolbox.register("population", tools.initRepeat, list, toolbox.particle)
 
pop = toolbox.population(n=5)

簡單遺傳演算法實現

前言

在上一篇中,我們已經介紹了如何在DEAP中實現進化演算法的基本操作,在這一篇中我們試圖將各個操作組裝起來,用進化演算法解決一個簡單的一元函數尋優問題。


進化演算法實例 - 一元函數尋優

問題描述與分析

給定一個函數h(x) = (x^2 + x)cos(2x)+x^2+x,求解該函數的最大值。

該函數的最大值應該出現在28.309042309042308處,值為1657.4235763265594

可以看到該函數有很多局部極值作為幹擾項,如果進化演算法過早收斂,很容易陷入某個局部最優。

問題的編碼與解碼

對於該問題,可以選取很多不同的編碼方式。本文計畫採用二進制編碼,精度要求到6位,那麼首先應當確定編碼的長度與解碼方式。由於有: 2^{25}<60*10^6<2^{26} 所以我們需要的二進制編碼長度為26。

利用DEAP自帶演算法求解

DEAP自帶的進化演算法介紹

DEAP自帶的演算法都比較基礎,通常可以用來測試問題描述、編碼方式和交叉突變操作組合的有效性。需要比較複雜的進化演算法時,可以通過在已經有的算子上進行擴充。

演算法描述
eaSimple簡單進化演算法
eaMuPlusLambda(\mu+\lambda)進化演算法
eaMuCommaLambda(\mu,\lambda)進化演算法

簡單進化演算法 :deap.algorithms.eaSimple

DEAP中預置的簡單進化演算法流程描述如下:

  1. 根據工具箱中註冊的toolbox.evaluate評價族群
  2. 根據工具箱中註冊的toolbox.select選擇與父代相同個數的育種個體
  3. 在族群中進行第一次循環,用工具箱中註冊的toolbox.mate進行配種,並用生成的兩個子代替換對應父代
  4. 在族群中進行第二次循環,用工具箱中註冊的toolbox.mutate進行變異,用變異後的子代替換對應父代
  5. 從1開始重複循環,直到達到設定的迭代次數

需要注意的是在這個過程中,生成子代有四種情況:受到配種影響;受到變異影響;既受到配種也受到變異影響;既不受配種影響也不受變異影響。

對應的偽程式碼可以表述為:

evaluate(population)
for g in range(ngen):
    population = select(population, len(population))
    offspring = varAnd(population, toolbox, cxpb, mutpb)
    evaluate(offspring)
    population = offspring

(\mu+\lambda)進化演算法:deap.algorithms.eaMuPlusLambda

該演算法的流程如下:

  1. 根據工具箱中註冊的toolbox.evaluate評價族群
  2. 在族群中進行循環,在每次循環中,隨機選擇crossover,mutation和reproduction三者之一:如果選擇到crossover,那麼隨機選擇2個個體,用工具箱中註冊的toolbox.mate進行配種,將生成的第一個子代加入到後代列表中,第二個子代丟棄;如果選擇到mutation,用工具箱中註冊的toolbox.mutate進行變異,將變異後的子代加入到後代列表中;如果選擇到reproduction,隨機選擇一個個體,將其複製加入到後代列表中
  3. 根據工具箱中註冊的toolbox.select,在父代+子代中選擇給定數量的個體作為子代
  4. 從1開始重複循環,直到達到設定的迭代次數

注意在這個子代生成的過程中,子代不會同時受到變異和配種影響

對應的偽程式碼可以表述為:

evaluate(population)
for g in range(ngen):
    offspring = varOr(population, toolbox, lambda_, cxpb, mutpb)
    evaluate(offspring)
    population = select(population + offspring, mu)

(\mu,\lambda)進化演算法:deap.algorithms.eaMuCommaLambda

(\mu+\lambda)基本相同,唯一的區別在於生成子代族群時,只在產生的子代中選擇,而丟棄所有父代。

對應的偽程式碼可以表述為:

evaluate(population)
for g in range(ngen):
    offspring = varOr(population, toolbox, lambda_, cxpb, mutpb)
    evaluate(offspring)
    population = select(offspring, mu)

呼叫DEAP自帶的進化演算法

在呼叫DEAP自帶的演算法時需要注意的是,由於內建演算法呼叫的alias已經提前給定,因此我們在register的時候,需要按照給定名稱註冊。

例如toolbox.register('crossover', tools.cxUniform)就不能被內建演算法識別,而應當按照要求,命名為mate,並且顯示給出交叉機率: toolbox.register('mate', tools.cxUniform, indpb = 0.5)

按照要求,使用預置的演算法需要註冊的工具有:

toolbox.evaluate:評價函數

toolbox.select:育種選擇

toolbox.mate:交叉操作

toolbox.mutate:突變操作

程式碼與測試

from deap import algorithms, base, creator, tools
from scipy.stats import bernoulli
import numpy as np
import random

random.seed(42)  # 確保可以復現結果

# 描述問題
creator.create("FitnessMax", base.Fitness, weights=(1.0,))  # 單目標,最大值問題
creator.create("Individual", list, fitness=creator.FitnessMax)  # 編碼繼承list類

# 個體編碼
GENE_LENGTH = 26  # 需要26位編碼

toolbox = base.Toolbox()
toolbox.register("binary", bernoulli.rvs, 0.5)  # 註冊一個Binary的alias,指向scipy.stats中的bernoulli.rvs,機率為0.5
toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.binary, n=GENE_LENGTH)  # 用tools.initRepeat生成長度為GENE_LENGTH的Individual

# 評價函數
def decode(individual):
   num = int("".join([str(_) for _ in individual]), 2)  # 解碼到10進制
   x = -30 + (num / (2**26 - 1)) * 60  # 對應回-30,30區間
   return x

def eval(individual):
   x = decode(individual)
   return (((x**2 + x) * np.cos(2 * x) + x**2 + x),)

# 生成初始族群
N_POP = 100  # 族群中的個體數量
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
pop = toolbox.population(n=N_POP)

# 在工具箱中註冊遺傳演算法需要的工具
toolbox.register("evaluate", eval)
toolbox.register("select", tools.selTournament, tournsize=2)  # 註冊Tournsize為2的錦標賽選擇
toolbox.register("mate", tools.cxUniform, indpb=0.5)  # 注意這裡的indpb需要顯示給出
toolbox.register("mutate", tools.mutFlipBit, indpb=0.5)

# 註冊計算過程中需要記錄的資料
stats = tools.Statistics(key=lambda ind: ind.fitness.values)
stats.register("avg", np.mean)
stats.register("std", np.std)
stats.register("min", np.min)
stats.register("max", np.max)

# 呼叫DEAP內建的演算法
resultPop, logbook = algorithms.eaSimple(pop, toolbox, cxpb=0.5, mutpb=0.2, ngen=50, stats=stats, verbose=False)

# 輸出計算過程
logbook.header = "gen", "nevals", "avg", "std", "min", "max"
print(logbook)

計算過程輸出(在保存的結果中,nevals代表該迭代中呼叫evaluate函數的次數;另外也可以令verbose=True直接在迭代中輸出計算過程):

gen nevals  avg     std     min         max    
0   100     279.703 373.129 -0.406848   1655.79
1   53      458.499 416.789 0.154799    1655.79
2   59      482.595 461.698 0.00177335  1655.79
3   52      540.527 472.762 -0.0472813  1655.79
4   59      580.49  514.773 0.0557206   1655.79
5   62      693.469 576.465 -0.393899   1655.79
6   71      875.931 609.527 0.000831826 1656.52
7   62      946.418 608.838 0.221249    1656.52
8   67      881.358 636.562 0.06291     1657.42
9   55      937.741 636.038 0.231018    1657.42
10  59      942.68  662.51  0.00094194  1657.42
11  50      990.668 641.284 1.36174     1657.42
12  49      1056.75 661.558 -0.392898   1657.42
13  60      1254.48 596.379 -0.374487   1657.42
14  56      1196.38 667.938 0.0073118   1657.42
15  61      1255.25 655.171 0.0130561   1657.42
16  57      1258.77 639.582 0.20504     1657.42
17  72      1328.39 587.822 11.0895     1657.42
18  66      1331.57 610.036 0.298211    1657.42
19  67      1297.66 632.126 -0.231748   1657.42
20  64      1246.8  610.818 0.0335697   1657.42
21  58      1099.19 690.046 0.0503046   1657.42
22  60      1068.91 664.985 0.0591922   1657.42
23  49      1243.47 612.2   0.00160086  1657.42
24  47      1231.47 658.94  -0.202828   1657.42
25  57      1233.48 657.109 0.0262275   1657.42
26  53      1410.59 534.754 0.877048    1657.42
27  71      1162.42 704.536 -0.257036   1657.42
28  52      1259.11 639.283 7.61025     1657.42
29  61      1312.6  615.661 1.40555     1657.42
30  56      1251.42 651.842 -0.0276344  1657.42
31  68      1200.02 678.454 0.00783006  1657.42
32  58      1198.2  675.925 -0.0726268  1657.42
33  61      1196.74 679.65  -0.135946   1657.42
34  60      1236.89 635.381 0.0133596   1657.42
35  50      1328.18 600.071 3.61031     1657.42
36  40      1357.62 599.045 -0.367127   1657.42
37  51      1299.96 633.932 -0.0674078  1657.42
38  48      1324.36 610.043 0.95255     1657.42
39  55      1323.23 566.232 0.987755    1657.42
40  58      1282.45 640.85  0.1648      1657.42
41  54      1335.26 593.431 -0.317349   1657.42
42  53      1330.78 593.921 0.0287983   1657.42
43  60      1332.02 597.941 1.32028     1657.42
44  65      1239.09 645.678 0.00144193  1657.42
45  71      1287.96 625.504 -0.372238   1657.42
46  51      1397.84 520.398 0.0150745   1657.42
47  66      1311.83 591.556 0.258935    1657.42
48  66      1160.49 694.509 0.163166    1657.42
49  50      1271.95 642.396 0.000437735 1657.42
50  61      1288.53 619.33  0.149628    1657.42

查看結果:

# 輸出最優解
index = np.argmax([ind.fitness for ind in resultPop])
x = decode(resultPop[index]) # 解碼
print('當前最優解:'+ str(x) + '\t對應的函數值為:' + str(resultPop[index].fitness))

結果:

當前最優解:28.308083985866368    對應的函數值為:(1657.4220524080563,)

可以看到遺傳演算法成功避開了局部最優,給出的結果非常接近全域最優解28.309了。

自行編寫演算法求解

程式碼與測試

自行編寫通常來說會需要比內建函數更長的篇幅,但是也能獲得更大的自由度。下面是一個用錦標賽交叉、位翻轉突變求解同樣問題的例子:

import random
import numpy as np
from deap import creator, base, tools
from scipy.stats import bernoulli

# 設定隨機種子以確保結果可復現
random.seed(42)

# 定義問題為單目標最大化
creator.create("FitnessMax", base.Fitness, weights=(1.0,))
creator.create("Individual", list, fitness=creator.FitnessMax)

# 定義基因編碼長度和工具箱
GENE_LENGTH = 26
toolbox = base.Toolbox()
toolbox.register("binary", bernoulli.rvs, 0.5)
toolbox.register(
    "individual", tools.initRepeat, creator.Individual, toolbox.binary, n=GENE_LENGTH
)


# 定義適應度評價函數
def eval(individual):
    num = int("".join([str(_) for _ in individual]), 2)
    x = -30 + num * 60 / (2**26 - 1)
    return (((x**2 + x) * np.cos(2 * x) + x**2 + x),)


toolbox.register("evaluate", eval)

# 初始化族群
N_POP = 100
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
pop = toolbox.population(n=N_POP)

# 評價初始族群適應度
fitnesses = map(toolbox.evaluate, pop)
for ind, fit in zip(pop, fitnesses):
    ind.fitness.values = fit

# 設定遺傳演算法參數
N_GEN = 50  # 最大代數
CXPB = 0.5  # 交叉概率
MUTPB = 0.2  # 突變概率

# 註冊遺傳運算子
toolbox.register("tourSel", tools.selTournament, tournsize=2)
toolbox.register("crossover", tools.cxUniform)
toolbox.register("mutate", tools.mutFlipBit)

# 設定記錄演算法狀態
stats = tools.Statistics(key=lambda ind: ind.fitness.values)
stats.register("avg", np.mean)
stats.register("std", np.std)
stats.register("min", np.min)
stats.register("max", np.max)
logbook = tools.Logbook()

# 開始遺傳迭代
for gen in range(N_GEN):

    # 選擇育種族群
    selectedTour = toolbox.tourSel(pop, N_POP)
    selectedInd = list(map(toolbox.clone, selectedTour))

    # 交叉
    for child1, child2 in zip(selectedInd[::2], selectedInd[1::2]):
        if random.random() < CXPB:
            toolbox.crossover(child1, child2, 0.5)
            del child1.fitness.values
            del child2.fitness.values

    # 突變
    for mutant in selectedInd:
        if random.random() < MUTPB:
            toolbox.mutate(mutant, 0.5)
            del mutant.fitness.values

    # 重新評價被改變個體
    invalid_ind = [ind for ind in selectedInd if not ind.fitness.valid]
    fitnesses = map(toolbox.evaluate, invalid_ind)
    for ind, fit in zip(invalid_ind, fitnesses):
        ind.fitness.values = fit

    # 更新族群
    pop[:] = selectedInd

    # 記錄當代狀態
    record = stats.compile(pop)
    logbook.record(gen=gen, **record)

# 輸出演算過程記錄
logbook.header = "gen", "avg", "std", "min", "max"
print(logbook)

計算過程輸出:

gen avg     std     min         max    
0   453.042 403.079 0.0541052   1533.87
1   494.408 419.628 0.860502    1626.41
2   543.773 465.404 -0.11475    1626.41
3   551.903 496.022 -0.0620545  1626.41
4   590.134 538.303 -0.0423912  1626.41
5   552.925 523.084 -0.348284   1544.09
6   580.065 532.083 0.000270764 1544.09
7   610.387 567.527 0.0294394   1574.38
8   667.447 556.671 -0.16236    1525.22
9   722.472 551.263 -0.39705    1524.89
10  698.512 548.705 0.0240564   1542.15
11  748.871 585.757 -0.405741   1542.15
12  717.023 580.996 0.00352434  1542.15
13  701.257 575.739 0.0797344   1542.15
14  720.325 593.033 -0.322945   1542.15
15  705.138 612.55  0.846468    1627.42
16  725.673 616.072 0.0259165   1641.05
17  683.245 609.737 -0.0759172  1630.68
18  613.711 613.64  -0.0448713  1627.25
19  648.058 579.645 -0.319345   1648.62
20  655.742 570.835 -0.247798   1613.19
21  692.022 541.547 -0.160214   1543.01
22  737.027 557.312 0.175429    1543.01
23  825.825 595.845 -0.403073   1542.95
24  831.611 600.052 -0.397646   1645.6 
25  991.433 564.623 0.095375    1641.16
26  842.529 641.466 -0.406455   1641.16
27  906.907 622.024 4.64596     1641.16
28  791.317 633.509 -0.234101   1641.16
29  896.355 598.829 0.339045    1641.16
30  781.726 625.868 0.0195279   1647.45
31  774.75  645.621 0.134108    1647.45
32  789.524 631.395 -0.388413   1647.45
33  985.497 631.871 0.00160795  1654.93
34  990.838 662.935 0.00156172  1657.21
35  1013.72 695.145 -0.266597   1652.41
36  1073.47 697.062 -0.0762617  1652.32
37  1118.98 667.797 0.508095    1652.32
38  1219.86 602.05  -0.256761   1652.39
39  1247.71 628.152 0.00243654  1652.39
40  1290.97 613.243 -0.374519   1652.39
41  1298.95 607.829 0.0630793   1652.41
42  1309.81 599.807 0.0235305   1652.47
43  1214.13 658.237 0.0027164   1653.03
44  1244.05 656.009 -0.329403   1653.13
45  1369.44 543.688 0.000339235 1653.05
46  1283.87 602.252 0.204417    1653.19
47  1157.28 686.628 0.00359533  1653.19
48  1284.39 630.622 0.0769602   1653.19
49  1301.76 608.879 0.0775233   1656.45

查看結果:

# 輸出最優解
index = np.argmax([ind.fitness for ind in pop])
x = decode(resultPop[index]) # 解碼
print('當前最優解:'+ str(x) + '\t對應的函數值為:' + str(pop[index].fitness))

結果輸出:

當前最優解:28.39357492914162 對應的函數值為:(1656.4466617035953,)

結果可視化

# 結果可視化
import matplotlib.pyplot as plt

gen = logbook.select('gen') # 用select方法從logbook中提取迭代次數
fit_maxs = logbook.select('max')
fit_average = logbook.select('avg')

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(gen, fit_maxs, 'b-', linewidth = 2.0, label='Max Fitness')
ax.plot(gen, fit_average, 'r-', linewidth = 2.0, label='Average Fitness')
ax.legend(loc='best')
ax.set_xlabel('Generation')
ax.set_ylabel('Fitness')

fig.tight_layout()
fig.savefig('Generation_Fitness.png')

演化式學習的基礎 — 基因演算法(GA)

出處 : https://medium.com/@deborah.deng/%E6%BC%94%E5%8C%96%E5%BC%8F%E5%AD%B8%E7%BF%92%E7%9A%84%E5%9F%BA%E7%A4%8E-%E5%9F%BA%E5%9B%A0%E6%BC%94%E7%AE%97%E6%B3%95-6e1a1a068c1b

img

在新工作中掙紮了一年,為了重回寫作世界,把之前演講的內容整理成文稿來發表。作者本身專業是OR(作業研究),念書時學GA是用來解優化問題;後來涉獵機器學習、AI應用。基本上「預測」這碼事也就是把預測數學模型運用「演算法」解算出來,再投入新資料把預測給算出來,在學習的過程中也有所謂優化問題,如「最小誤差」。所以GA也可應用於ML、AI中,差別在於OR的應用多半是現實問題,而ML/AI的應用傾向於純數學。但作者認為 — 瞭解OR能夠對瞭解ML、AI有更深入的理解。

本文分為三大主題,從「優化問題」開始、再簡單介紹「GA(基因演算法)」的流程及方法,最後說明GA在「機器學習」上的應用方式。希望能幫助到想要往相關領域發展或評估應用的朋友們。有任何問題或指教,歡迎在下方留言。

優化問題概述

數學上的「優化問題(Optimization Problems)」是將現實世界的「決策」轉化為數學式子(模型),再運用各種「演算法」將「最佳解」給找出來,這個過程稱為「解算(Solving)」。這麼講可能有點模糊,以下我們用「購屋、租屋」這件事來描繪什麼是個「優化問題」,又如何找到「最佳解」:

房屋搜尋

在臺灣的朋友必定不陌生,找房子大概會到591這樣的網站搜索,美國的話一般會到Redfin、Zillows等。當然網站介面上提供一系列的篩選條件,使用者點選後,下方即出現篩選結果。使用者再依據自己的喜好,在清單上過濾,最後找到幾個想要繼續探索的標的 — 這個過程即是「決策優化」的過程。我猜大家在過濾清單時,腦中一定浮現各式條件,有些可能不在網站提供的條件中。在看到清單,到挑出有興趣的那幾個標的,即是「優化問題」涵蓋的範圍;而最後決定下來的那些標的,也就是每個人心中的「最佳解」。

不過,問題瞭解後得轉換成數學式子,才能開始計算。所以在「建構」優化問題時有2大區塊須加以定義:「欲決定的問題」及「追求標的的限制條件」,其後才能據以定義模型中的「目標函數」及「限制式」,請參考以下範例:

img

凡事變成數學,就得轉換成數字可描述或代表的。比如在「欲決定的問題」中,就必須充分定義心中的「最佳解」是什麼光景,有些人追求「最低房價」、有些人追求「最佳生活機能」或「交通便利」。而限制式中也各不相同,有些可能要「最小坪數」,有些人只要「一房一廳」,這些就Case-By-Case,需要花時間瞭解。所以一個優化模型的建立,在前期是需要與使用者充分溝通,以確保後續模型建置的正確性。

優化問題到模型建置

問題定義之後需將模型建置的幾項「要素」加以確立,分別是「決策變數」、「目標函數」及「限制式」。範例如下:

img

用「房屋搜尋」問題來說明,我們想知道的是:「現有房源中,有哪些是符合我們的標的」;假設現有有5間房屋,我們想知道是第2間符合我們的期望、還是第5間符合,用數學變數來代表這樣的解答其實很簡單 — 用(0,1)即可:第1間符合 x1=1, 不符合x1=0;第2間符合x2=1, 不符合x2=0 — 最後的最佳解即用(0,1,0,0,0)代表第2間符何,而(0,0,0,0,1)代表第5間符合。而「決策變數」則以(x1, x2, x3, x4, x5)來代表。

決策變數定好了就可開始定義「目標函數」,這時我們需思考「決策」或「決定」的「評估標準」為何?如前述,有些人可以能要價格評估、有些人是以「質化」的標準如環境良好、交通便利等方式評估。因為目標函數的最終目標是用最大化或最小化來評估各方案,所以必須先搞清楚評估準則為何,再依準則來轉換目標函數,之後解算模型時才能產出正確的結果。用最簡單的最低房價做為評估準則,則前述5間房情境的目標函數即是:p1*x1 + p2*x2 + … + p5*5p1, p2,…, p5代表各房之價格。

現實生活總有諸多限制,所以描繪現實決策的數學模型也要有「限制式」。如房屋網站中的搜尋條件,如果只要2臥房的標的,則優化限制需要有 r1*x1 <= R、 r2*x2 <= R、……、r5*x5 <= R,where R=2。這樣,如果第1間房是最低價,但其僅有1間臥房,此標的即在優化過成中排除。限制式係按需要納入,可以有很多條。

以下即是一個房屋評估的優化決策模型 — 找出符合預算、坪數及房數期望的最低價房屋標的:

img

另外讓大家比較有感(不要被一串數學式子嚇到),用個例子來說明。假設A先生想用不超過1500萬的預算找到至少15坪2間房最低價物件,用3個方案來做選擇的情境如下,可看出符合所有條件的最佳方案是下是A物件:

img

不過,只有3個物件還容易用心算,現實中10幾個物件、或者成千上萬的方案要怎麼找最佳解呢?還有,限制條件在限實生活中可不只3項,更多更複雜又怎麼辦呢?這就是在OR領域中要學演算法的原因了。

淺談基因演算法

說到演算法,我想大家國中都學過二元一次三元一次方程式(解雞免同籠的那種);演算法其實類似其求解方法,經過一系列的運算,最後得出x及y的值。在解算上述的優化問題時,最基本的叫Simplex Method,是用矩陣運算將決策變數一一推算 — 這種方法叫做解析解(Analytic Solution),好處是保證找得出最佳解,但壞處是運算時間長。尤其在前面提及的優化問題情境中,可能包括千百個變數,有時候在解算時會發現掉入NP-Hard的麻煩中 — 也就是電腦計算時間非常長,無法在合理的時間內完成求解。

演算法可快速解算NP-Hard問題

下圖是一個針對「太陽能選址評估」的情境;圖中僅有2個用地、3個饋線,試想用地數、饋線數增加時將達到的方案數量,用電腦算也要算很久很久(課本上的例子都說會算好幾年了) — 這就是所為的NP-Hard

img

想深入探討的讀者可參考這篇:「Non-deterministic Polynomial-time Hardness in computational complexity theory」。

又提一下網路商店的應用(如亞馬遜),試想在幾百個倉庫中要找到最佳出貨點,每次出貨時求解時間要1週,大概沒要人想要採用這種系統吧。因此,科學家們發明出Metaheuristics的演算法來解決這個困難,而今天要介紹的「基因演算法(Genetic Algorithm)」即是其中一種。

GA的定義及機制

維基百科中描述:In computer science and operations research, a genetic algorithm (GA) is a metaheuristic inspired by the process of natural selection that belongs to the larger class of evolutionary algorithms (EA). Genetic algorithms are commonly used to generate high-quality solutions to optimization and search problems by relying on biologically inspired operators such as mutation, crossover and selection.

Meta-是「之後」的意思,而heuristic則是「搜尋」 — 這類演算法有個特點, 在隨機搜尋之後,都有個方法導向最佳解,達到快速解算的目的。而基因演算法即是模仿「生物演化」的方式,先挑選2個隨機解當父母,再分別切割出傳承染色體,再進行配對。以「物競天擇」之天然選擇概念,將優良基因衍生至下一代,藉以產生優良後代, 逐步找到最佳解。

而所謂基因、染色體等概念可參考下圖,基因(Gene)組成染色體(Chromosome),各式染色體組成群體(Population)。而不同染色體透過隨機抽樣、切割,再組成新的染色體,不斷繁衍:

img

上圖及後續說明圖片來源為: Toward Data Science — Intro to Genetic Algorithm by Vijini Mallawaarachchi ,也是篇說明簡潔的文章可以參考。

用找房情境來說明,基因代表的即是入選與否,以0或1表示。而染色體則是整個解(或方案),如(0,0,0,1,0)或(0,1,0,0,0) 分別代表第4物件入選及第2物件入選。而整體流程如下圖,基本上就是多少小孩,突變產生優良基因、有效保留優良基因,持續繁衍:

img

在繁衍後代時,先隨機選押切割點,將父母基因切割、進行交配(互換),即可產出子代:

img

而突變則用隨機選擇並改變部分基因,來產出子代如下圖:

img

產出子代後演算法用目標函數或Fitness Function進行評估,若子代比較好則保留(如找到更低價的方案),沒有則棄之。但一代代的子孫繁衍後可能會出現一種情況 — 落入Local Optimal (見置頂圖),這也是需要進行突變的原因之一,可以幫助搜尋跳出現有搜尋範圍,繼續往Global Optimal前進。

基因演算法與機器學習

最後來說明怎麼在機器學習上應用GA — 基本上分為2大類:特徵選取(Feature Selection)及模型調校(Model Tuning)。分別簡單說如下:

特徵選取(Feature Selection)

先用個小例子介紹一下預測模型。近年綠能產業很熱門,不如說說太陽能發電量預測吧!發電量與天氣習習相關,所以可以用日射量、氣溫、濕度、風向、降雨量等氣象資料來建立預測模型。預測模型在數學上用 y=B1*X1+B2*X2+…+Bn*Xn來表示,所謂「建模」就是用收集到的資料(訓模資料)丟進模型中,再用演算法找出最佳的係數( B1, B2,…Bn),最後再用新資料投入建好的模型中,預測未來。接下來用下圖說明:

img

假設我們的訓模資料有5個欄位(變數),但不見得所有變數都是必要的,所以建模的過程會選擇某幾個變數投入,建成不同的模型後,再比較各模型的準確度 — 這個過程即是「特徵選取」。5個變數能建出的模型共有:5 + 10 + 10 + 5 + 1 (分別為選取1, 2, 3, 4, 5個變數) = 31種組合 → population;而0與1仍代表該變數是否被選取 →基因;5個變數被選取的組合 → 染色體。這樣的概念下就可以運用基因演算法,隨機從Population中選擇父母,再進行交配、突變等過程,經由比較預測模型的Fitness function來決定最佳模型。

當然,31個組合有點不符現實,通常會有成千上百的選擇。有興趣細節的讀者也可參考這篇: Feature Selection using Genetic Algorithms in R

模型調校(Model Tuning)

另一種應用是模型調校,我想很多人一定聽過如隨機森林、類神經網路、XGBoost等等 — 這些是偉大的電腦科學家發明的各式機器學習演算法,而每種方法按其原理有各式超參數(Hyperparameters)可以調校 — 有興趣可以參考這個網站:Available Models in R Caret Package — 其中羅列了滿完整的機器學習模型及其參數。

以類神經網路(Neural Network)來說,可調整的超參有*Learning rate、Number of layers、*Batch Size等等。而每個參數中又有好幾種選擇,各種組合加幾來也有幾百個模型可以選。下圖列出部份組合,大家應可感受到數量的龐大了吧!

img

目前這篇已經很長了,我就不多介紹,大家可以參考「Hyperparameters Tuning in Neural Networks」及「Fine-Tuning A Neural Network Explained」。

各式的組合整理出來,應可讓大家聯想回GA的例子。還好有電腦的發明及科學家的努力,現在可以利用這些工具來搜索這組合數非常恐怖多的問題。用以上各種模型作為GA的基因及染色體,接下來就可按下圖GA的步驟產出數個NN模型,繁衍、評估、直到找到最佳模型 — 也就是最後產出的預測模型了。

img

想了解細節的讀者,可以參考資料來源:「Genetic Algorithms + Neural Networks = Best of Both Worlds」。

後記

大體來說,GA適合的情境即是在大量選擇中尋求最佳解答,雖說這只是Metaheuristics中的一種方法,但作者認為容易理解,所以也容易學會及執行。但要跟大家強調一點,這類搜尋法的優點是「快」,但是「不保證」能找到「最佳解」。這也是在應用時需要注意的事,通常我們會找幾個能用Analytics solution解出來的案例來比較,確保開發出來的演算法能夠找到最接近最佳解的答案!

本文內容原為2021年臺灣人工智慧發展協會之AI女力講座;雖日期稍久,但內容為基礎原理無大礙;作者希望用淺顯的語言、簡單的例子,讓各界有心朝AI發展的同好們能更容易瞭解AI背後的運作機制。另外,也可作為瞭解更高深的[Evolutionary Programming](https://en.wikipedia.org/wiki/Evolutionary_programming#:~:text=Evolutionary programming is one of,parameters are allowed to evolve.) 的跳板。如有任何指教,歡迎留言討論。感謝您閱讀。

機器學習常勝軍 - XGBoost

https://ithelp.ithome.com.tw/articles/10273094

XGBoost

今日學習目標

  • XGBoost 介紹
    • XGBoost 是什麼?為什麼它那麼強大?
    • XGBoost 優點
  • 比較兩種整體學習架構差異?
    • Bagging vs. Boosting
    • Boosting vs. Decision Tree
  • Boosting 方法有哪些
  • 實作 XGBoost 分類器與迴歸器
    • 比較 Bagging 與 Boosting 兩者差別

人人驚奇的 XGBoost

XGboost 全名為 eXtreme Gradient Boosting,是目前 Kaggle 競賽中最常見到的算法,同時也是多數得獎者所使用的模型。此機器學習模型是由華盛頓大學博士生陳天奇所提出來的,它是以 Gradient Boosting 為基礎下去實作,並添加一些新的技巧。它可以說是結合 Bagging 和 Boosting 的優點。XGboost 保有 Gradient Boosting 的做法,每一棵樹是互相關聯的,目標是希望後面生成的樹能夠修正前面一棵樹犯錯的地方。此外 XGboost 是採用特徵隨機採樣的技巧,和隨機森林一樣在生成每一棵樹的時候隨機抽取特徵,因此在每棵樹的生成中並不會每一次都拿全部的特徵參與決策。此外為了讓模型過於複雜,XGboost 在目標函數添加了標準化。因為模型在訓練時為了擬合訓練資料,會產生很多高次項的函數,但反而容易被雜訊幹擾導致過度擬合。因此 L1/L2 Regularization 目的是讓損失函數更佳平滑,且抗雜訊幹擾能力更大。最後 XGboost 還用到了一階導數和二階導數來生成下一棵樹。其中 Gradient 就是所謂的一階導數,而 Hessian 即為二階導數。

XGBoost 優點

XGBoost 除了可以做分類也能進行迴歸連續性數值的預測,而且效果通常都不差。並透過 Boosting 技巧將許多弱決策樹集成在一起形成一個強的預測模型。

  • 利用了二階梯度來對節點進行劃分
  • 利用局部近似算法對分裂節點進行優化
  • 在損失函數中加入了 L1/L2 項,控制模型的複雜度
  • 提供 GPU 平行化運算

Bagging vs. Boosting

在這裡幫大家回顧一下整體學習中的 Bagging 與 Boosting 兩者間的差異。首先 Bagging 透過隨機抽樣的方式生成每一棵樹,最重要的是每棵樹彼此獨立並無關聯。先前所提到的隨機森林就是 Bagging 的實例。另外 Boosting 則是透過序列的方式生成樹,後面所生成的樹會與前一棵樹相關。本章所提及的 XGBoost 就是 Boosting 方法的其中一種實例。正是每棵樹的生成都改善了上一棵樹學習不好的地方,因此 Boosting 的模型通常會比 Bagging 還來的精準。

  • Bagging 透過抽樣的方式生成樹,每棵樹彼此獨立
  • Boosting 透過序列的方式生成樹,後面生成的樹會與前一棵樹相關

Boosting vs. Decision Tree

我們再與最一開始所提的決策樹做比較。決策樹通常為一棵複雜的樹,而在 Boosting 是產生非常多棵的樹,但是每一棵的樹都很簡單的決策樹。Boosting 希望新的樹可以針對舊的樹預測不太好的部分做一些補強。最終我們要把所有簡單的樹合再一起才能當最後的預測輸出。

Boosting 方法有哪些

AdaBoost 是由 Yoav Freund 和 Robert Schapire 於 1995 年提出。所謂的自適應是表示根據弱學習的學習誤差率表現來更新訓練樣本的權重,然後基於調整權重後的訓練集來訓練第二個弱學習器,藉由此方法不斷的迭代下去。

Gradient Boosting 由 Friedman 於 1999 年提出。其中 GBDT (Gradient Boosting Decision Tree) 的弱學習器僅限於只能使用 CART 決策樹模型,並採用加法模型的前向分步算法來解決分類和迴歸問題。

接下來介紹三個近年三個強大的開源機器學習專案。首先 XGBoost 最初是由陳天奇於 2014 年 3 月發起的一個研究項目,並在短時間內成為競賽中的熱門的模型。接著於 2017 年 1 月微軟發布了第一個穩定的 LightGBM 版本。它是一個基於 Gradient Boosting 的輕量級的演算法,優點在於使用少量資源、更快的訓練效率得到更好的準確度。另外在同年的 4 月,俄羅斯的一家科技公司 Yandex 發布了 CatBoost,其核心依然使用了 Gradient Boosting 技巧,並為類別型的特徵做特別的轉換並產生新的數值型特徵。

未來幾天將會介紹 LightGBM 與 CatBoost 哦!

[程式實作]

XGBoost 分類器

Parameters:

  • n_estimators: 總共迭代的次數,即決策樹的個數。預設值為100。
  • max_depth: 樹的最大深度,默認值為6。
  • booster: gbtree 樹模型(預設) / gbliner 線性模型
  • learning_rate: 學習速率,預設0.3。
  • gamma: 懲罰項係數,指定節點分裂所需的最小損失函數下降值。

Attributes:

  • feature_importances_: 查詢模型特徵的重要程度。

Methods:

  • fit: 放入X、y進行模型擬合。
  • predict: 預測並回傳預測類別。
  • score: 預測成功的比例。
  • predict_proba: 預測每個類別的機率值。
from xgboost import XGBClassifier

# 建立 XGBClassifier 模型
xgboostModel = XGBClassifier(n_estimators=100, learning_rate= 0.3)
# 使用訓練資料訓練模型
xgboostModel.fit(X_train, y_train)
# 使用訓練資料預測分類
predicted = xgboostModel.predict(X_train)

使用Score評估模型

我們可以直接呼叫 score() 直接計算模型預測的準確率。

# 預測成功的比例
print('訓練集: ',xgboostModel.score(X_train,y_train))
print('測試集: ',xgboostModel.score(X_test,y_test))

輸出結果:

訓練集:  1.0
測試集:  0.9333333333333333

大家可以試著與前幾天的決策樹和隨機森林兩個模型相比較。是不是 XGBoost 有著更好的預測結果呢?因為有了 Gradient Boosting 學習機制,大幅提升了預測能力。在學習過程中將預測不好的地方,尤其是橘色 (Versicolour) 與綠色 (Virginica) 交界處有更好的評估能力。

XGBoost (迴歸器)

Parameters:

  • n_estimators: 總共迭代的次數,即決策樹的個數。預設值為100。
  • max_depth: 樹的最大深度,默認值為6。
  • booster: gbtree 樹模型(預設) / gbliner 線性模型
  • learning_rate: 學習速率,預設0.3。
  • gamma: 懲罰項係數,指定節點分裂所需的最小損失函數下降值。

Attributes:

  • feature_importances_: 查詢模型特徵的重要程度。

Methods:

  • fit: 放入X、y進行模型擬合。
  • predict: 預測並回傳預測類別。
  • score: 預測成功的比例。
  • predict_proba: 預測每個類別的機率值。
import xgboost as xgb

# 建立 XGBRegressor 模型
xgbrModel=xgb.XGBRegressor()
# 使用訓練資料訓練模型
xgbrModel.fit(x,y)
# 使用訓練資料預測
predicted=xgbrModel.predict(x)

Reference

https://github.com/andy6804tw/2021-13th-ironman

深度學習-物件偵測:You Only Look Once (YOLO)

https://chih-sheng-huang821.medium.com/%E6%B7%B1%E5%BA%A6%E5%AD%B8%E7%BF%92-%E7%89%A9%E4%BB%B6%E5%81%B5%E6%B8%AC-you-only-look-once-yolo-4fb9cf49453c

You Only Look Once (YOLO)這個字是作者取自於You only live once,YOLO是one stage的物件偵測方法,也就是只需要對圖片作一次 CNN架構便能夠判斷圖形內的物體位置與類別,因此提升辨識速度。對於one stage和two stage是什麼可以參考: 深度學習-什麼是one stage,什麼是two stage 物件偵測

三個YOLO重要的步驟

1.Resize輸入的圖到448*448

  1. 執行一個卷積神經網路
  2. 基於模型輸出的信心程度(Confidence)依據閾值和Non-max suppression得到偵測結果

img

這篇文章結構會分成

  • YOLO用的卷積神經網路
  • YOLO物件偵測怎麼做的
  • YOLO怎麼training

YOLO用的卷積神經網路

YOLO的卷積網路架構是來自GoogleNet的模型,YOLO的網路有24卷積層(convolutional layer)和2層全連結層(fully connected layer),和GoogleNet不同的地方在於作者在某些3×3的卷積層前面用1×1的卷積層來減少filter數量,整體架構如下圖。

Note: 1×1的卷積層通常拿來做降維度的作用,可以減低計算量,且不影響太多mAP。有興趣可以參考: 卷積神經網路(Convolutional neural network, CNN): 1×1卷積計算在做什麼

img

YOLO物件偵測怎麼做的

白話說就是: YOLO在物件偵測部分基本上就是將圖拆成很多個grid cell,然後在每個grid cell進行2個bounding box的預測和屬於哪個類別的機率預測,最後用閾值和NMS (Non-Maximum Suppression) 的方式得到結果。

實際做法: YOLO如何得到很多個可能物件的信心程度、機率和bounding boxes

假設輸入圖的大小是100×100,總共偵測C個物件,YOLO最後輸出的tensor的大小是S×S×(B×5+C)←後面會開始講這個東西怎麼來的。

  1. YOLO會把圖先平均分成S×S格,這邊假設S = 5,圖會被平均分成5×5格(如上左圖),每一格在英文被稱為grid cell (大小為20×20)。

整體的概念就是如果要「被偵測的物件中心」落在哪一個grid cell,那個grid cell就要負責偵測這個物件

  1. 每個grid cell必須要負責預測「*B*個bounding boxes」和「屬於每個類別的機率」,每個bounding box會帶有5個預設值(x, y, w, h, and confidence)

(x, y)用來表示某一個物件在這個grid cell的中心座標,這個物件相對應的寬高分別為w, h。而confidence則是用來表示這個物件是否為一個物件的信心程度(confidence score)。

Confidence score在YOLO這篇文章內計算方式被定義為

img

所以從公式看就很直覺,如果在某個grid cell沒有任何物件,這時候confidence score就會是0,反之如果物件在grid cell內,最好的情況就是confidence score等於預測的bounding box和ground truth的IOU (intersection over union)。實際上confidence預測是預測bounding box和ground truth的IOU。

2.1 每個grid cell還需要負責預測「屬於每個類別的機率」,所以每個grid cell還會有C個條件機率(conditional class probabilities,Pr(Class|Object)),從公式很明顯就知道這個條件機率就是「這個grid cell內包含有一個物件這個物件屬於某一類的機率」。

這邊有一個地方跟新的方法不同的地方,也是這邊被後來YOLO版本修正的地方→YOLO的每個grid cell只預測一組類別的條件機率,換言之每個grid cell雖然有B個bounding box,但實際上只從B個bounding box中預測一個物件和這個物件屬於哪一類的機率。

2.2 這測試(test)階段時,實際上每個bounding box會得到一組class-specific confidence scores,計算方式如下:

img

  1. 作者舉PASCAL VOC在執行YOLO的例子,他設定S=7,B=2,PASCAL VOC有20個物件的類別,所以C=20。一開始有說YOLO最後的tensor為S×S×(B×5+C)。以VOC的例子來說輸出為7×7×(2×5+20)= 7×7×30。

I. 這邊的7×7就是7×7個grid cell,每一個grid cell都會對應到原始圖的相對位置。 II. 2×5就是兩個Bounding box各自帶有5個數值分別為bounding box的中心座標(x,y)和寬長(w, h)和confidence score。 III. 20就是屬於20個類別的機率。

Bounding box怎麼找物件和分類

下圖為YOLO作者放在論文中的解釋最後Bounding box怎麼找物件和分類的圖。

img

一般論文篇幅有限,加上作者會認為會看物件偵測的人基本上有一點common sense,所以會一張圖解釋所有的東西,這邊我把圖拆掉來說明。

  1. Bounding Box在每個grid cell怎麼看,如下圖,紅色點為這個被偵測可能是物件的中心點,相對應的長寬可以框出紅色的框,這個就是Bounding box。

Note: 我這段話寫「紅色點為這個被偵測可能是物件的中心點」,原因是我實際框出來的是狗的位置,但實際上YOLO的輸出Bounding box在每個grid cell是一定存在,所以有可能Bounding box框出的是背景,但YOLO的每個Bounding box都帶有一個confidence score,這個confidence score可以來決定這個Bounding box是否真的是物件。→基本上大多數的物件偵測都是這樣做。

img

  1. 根據YOLO作者的想法整張圖一共最多有98個Bounding Box,也就是實際最多只能偵測98個物件,然後全部候選的Bounding box的(中心座標、長寬和confidence score)根據閾值和NMS選出這張圖所有的物件(如下圖的紅色框、紫色框和土黃色框)。

Note: 在YOLO作者的例子(7×7×(2×5+20)),共有7×7=49個grid cell,每個grid cell最多有2個Bounding box,所以全部候選的bounding box共有7×7×2=98個。

這98個Bounding Box的Confidence score先經由一個閾值(threshold),先幹掉一些確定不是物件的Bounding box,可以減少後面NMS的計算。然後後面再用NMS(之後會再介紹NMS)的方式把一些重疊的Bounding Box做一個消除,重覆執行直到每個類別都完成,剩下來的Bounding Box就是選出來的物件。

  1. YOLO同時間的輸出來還有(7×7×20),這個代表每一個grid cell內每一類的機率,這時候取機率最大的那一類代表這個grid cell的類別(下中圖)。
  2. 這時候結合「步驟2選出的物件」和「步驟3對應的grid cell是什麼類別」,就可以決定這個選出的物件屬於什麼類別(下右圖)。

img

YOLO怎麼training

Pretain:

YOLO作者用ImageNet 1000-class competition dataset來pretrain模型,但作者只訓練前20卷積層(上面paper節錄的Figure 3)後面接上一個average-pooling layer和一個全連結層(top-5 accuracy為88%)。

這邊蠻好玩的,作者自己做了一個稱為Darknet的framework來做全部的training和inference(只能說大神都很強)。

Bounding Box正規化

YOLO的最後一層預測的是類別機率和bounding box等 在YOLO訓練前會先依據輸入圖的長寬,正規化(normalize)bounding box的長寬,因此bounding box的長寬會介於0~1之間。 Bounding box的中心座標(x,y)是在特定grid cell的偏移(offset),所以座標也會介於0~1之間。

Activation function

整個YOLO架構除了最後一層用線性輸出為,每一層都會搭配leaky rectified linear activation (leaky ReLU)

img

Loss function

一般都用平方誤差和(sum-squared error)當作loss function,原因是容易最佳化(可以參考倒傳遞相關文章)。作者認為此方法不能完美校正去最大化目標的平均精度(average precision),主要原因是每項目的error(比如bounding box的定位誤差(localization error)和分類的誤差(class error))都佔有一樣的比重,所以結果不太好(我猜作者應該是試過equal weight,所以他在文章寫in every image many grid cells do not contain any object.),而且物件偵測多數情況,大多數的grid cell內是沒有物件的(在梯度求解的時候容易將有物件的cell壓過去),所以容易導致confidence幾乎趨近於0,也因為如此容易造成模型不穩定。

為了解決這個問題,作者

  1. 增加了在bounding box座標預測的loss權重 (λcoord=5)
  2. 減低那些不包含物件的Box,confidence預測時的權重(λnoobj=0.5)

YOLO multi-part loss function定義如下,老實說一開始都很難看的懂(實際上真的很難看的懂@@):

img

如此一來此loss function只會針對物件如果有出現在某個grid cell下進行懲罰分類錯誤。並且只會針對有責任於偵測Ground truth box的預測者進行懲罰bounding box錯誤,也就是grid cell中有最高IOU的預測者。

結論

Performance 這部分直接看論文吧。

這邊作者有提到YOLO的Limitations → 所以才有YOLOv2出來啊。

  1. 第一點也是我前面有提到YOLO對Bounding box有很強烈的空間限制,也就是每個grid cell只有最多只有2個bounding box和一個類別,也是因為這點所以如果有兩個以上的物件在空間上離的非常近會導致模型無法有效偵測到,比如說有一群鳥(一群小物件)。
  2. 第二bounding box的預測是從資料學來的,所以如果訓練好的模型要去預測其他新物件或是比例很怪的物件,可能就沒有辦法框的很好。這部份原因來自於YOLO本身有很多層的Pooling (downsampling),最後得到的feature用來預測bounding box,相較於原始圖在空間上是比較粗略的。
  3. 最後一點,作者提出的loss function在小物件和大物件的Bounding Box都用一樣的比重,但實際上在計算IOU時,小物件只要差一點點,定位(localization)的error影響就會很大,相對的大物件而言就比較沒有太大差異,作者提到主要的錯誤都是來自不正確的定位(incorrect localizations)。

本文將手把手教你用YoloV8訓練自己的資料集並實現手勢識別

安裝環境

【1】安裝torch, torchvision對應版本,這裡先下載好,直接安裝

pip install torchvision-0.14.1+cu116-cp38-cp38-win_amd64.whl
pip install torch-1.13.1+cu116-cp38-cp38-win_amd64.whl

安裝好後可以查看是否安裝成功,上面安裝的gpu版本,查看指令與結果:

import torch
print(torch.__version__)
print(torch.cuda.is_available())

【2】安裝ultralytics

pip install ultralytics

【3】下載YoloV8預訓練模型:GitHub - ultralytics/ultralytics: NEW - YOLOv8 🚀 in PyTorch > ONNX > OpenVINO > CoreML > TFLite

img

【4】運行demo測試安裝是否成功:

from ultralytics import YOLO
# Load a model
model = YOLO('yolov8n.pt')  # pretrained YOLOv8n model
 
# Run batched inference on a list of images
results = model(['1.jpg', '2.jpg'])  # return a list of Results objects
 
# Process results list
for result in results:
    boxes = result.boxes  # Boxes object for bounding box outputs
    masks = result.masks  # Masks object for segmentation masks outputs
    keypoints = result.keypoints  # Keypoints object for pose outputs
    probs = result.probs  # Probs object for classification outputs
    result.show()  # display to screen
    result.save(filename='result.jpg')  # save to disk

標註/製作資料集

【1】準備好待標註圖片

可以自己寫一個從攝影機存圖的指令碼保存一下不同手勢圖到本地,這裡提供一個供參考:
import cv2
 
cap = cv2.VideoCapture(0)
flag = 0
 
if(cap.isOpened()): #視訊打開成功
  flag = 1
else:
  flag = 0
  print('open cam failed!')
 
if(flag==1):
  while(True):
    cv2.namedWindow("frame")
    ret,frame = cap.read()#讀取一幀
    if ret==False: #讀取幀失敗
      break
    cv2.imshow("frame", frame)
    if cv2.waitKey(50)&0xFF ==27: #按下Esc鍵退出
      cv2.imwrite("1.jpg",frame)
      break
 
cap.release()
cv2.destroyAllWindows()

本文使用共3種手勢1,2,5,三種手勢各300張,大家可以根據實際情況增減樣本數量。

img

【2】標註樣本

標註工具使用labelimg即可,直接pip安裝:

pip install labelimg

安裝完成後,命令列直接輸入labelimg,Enter即可打開labelimg,資料集類型切換成YOLO,然後依次完成標註即可。

img

【3】標註劃分

標註好之後,使用下面的指令碼劃分訓練集、驗證集,注意設定正確的圖片和txt路徑:

import os
import random
import shutil
 
# 設定檔案路徑和劃分比例
root_path = "./voc_yolo/"
image_dir = "./JPEGImages/"
label_dir = "./Annotations/"
train_ratio = 0.7
val_ratio = 0.2
test_ratio = 0.1
 
# 建立訓練集、驗證集和測試集目錄
os.makedirs("images/train", exist_ok=True)
os.makedirs("images/val", exist_ok=True)
os.makedirs("images/test", exist_ok=True)
os.makedirs("labels/train", exist_ok=True)
os.makedirs("labels/val", exist_ok=True)
os.makedirs("labels/test", exist_ok=True)
 
# 獲取所有圖像檔案名稱
image_files = os.listdir(image_dir)
total_images = len(image_files)
random.shuffle(image_files)
 
# 計算劃分數量
train_count = int(total_images * train_ratio)
val_count = int(total_images * val_ratio)
test_count = total_images - train_count - val_count
 
# 劃分訓練集
train_images = image_files[:train_count]
for image_file in train_images:
    label_file = image_file[:image_file.rfind(".")] + ".txt"
    shutil.copy(os.path.join(image_dir, image_file), "images/train/")
    shutil.copy(os.path.join(label_dir, label_file), "labels/train/")
 
# 劃分驗證集
val_images = image_files[train_count:train_count+val_count]
for image_file in val_images:
    label_file = image_file[:image_file.rfind(".")] + ".txt"
    shutil.copy(os.path.join(image_dir, image_file), "images/val/")
    shutil.copy(os.path.join(label_dir, label_file), "labels/val/")
 
# 劃分測試集
test_images = image_files[train_count+val_count:]
for image_file in test_images:
    label_file = image_file[:image_file.rfind(".")] + ".txt"
    shutil.copy(os.path.join(image_dir, image_file), "images/test/")
    shutil.copy(os.path.join(label_dir, label_file), "labels/test/")
 
# 生成訓練集圖片路徑txt檔案
with open("train.txt", "w") as file:
    file.write("\n".join([root_path + "images/train/" + image_file for image_file in train_images]))
 
# 生成驗證集圖片路徑txt檔案
with open("val.txt", "w") as file:
    file.write("\n".join([root_path + "images/val/" + image_file for image_file in val_images]))
 
# 生成測試集圖片路徑txt檔案
with open("test.txt", "w") as file:
    file.write("\n".join([root_path + "images/test/" + image_file for image_file in test_images]))
 
print("資料劃分完成!")

接著會生成劃分好的資料集如下:

圖片

打開images資料夾:

圖片

打開images下的train資料夾:

圖片

打開labels下的train資料夾:

圖片

訓練與預測

【1】開始訓練

訓練指令碼如下:

from ultralytics import YOLO
# Load a model
model = YOLO('yolov8n.pt')  # load a pretrained model (recommended for training)
 
results = model.train(data='hand.yaml', epochs=30, imgsz=640, device=[0],
                      workers=0,lr0=0.001,batch=8,amp=False)

hand.yaml內容如下,注意修改自己的資料集路徑即可:

# Ultralytics YOLO 🚀, AGPL-3.0 license
# COCO8 dataset (first 8 images from COCO train2017) by Ultralytics
# Documentation: https://docs.ultralytics.com/datasets/detect/coco8/
# Example usage: yolo train data=coco8.yaml
# parent
# ├── ultralytics
# └── datasets
#     └── coco8  ← downloads here (1 MB)
 
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
path: E:/Practice/DeepLearning/Yolo_Test/dataset/hand # dataset root dir
train: E:/Practice/DeepLearning/Yolo_Test/dataset/hand/images/train # train images (relative to 'path') 4 images
val: E:/Practice/DeepLearning/Yolo_Test/dataset/hand/images/val # val images (relative to 'path') 4 images
test: # test images (optional)
 
# Classes
names:
  0: hand-1
  1: hand-2
  2: hand-5
 
 
# Download script/URL (optional)
# download: https://ultralytics.com/assets/coco8.zip

CPU訓練將device=[0]改為device='cpu'即可

訓練完成後再runs/detect/train資料夾下生成如下內容:

在weights資料夾下生成兩個模型檔案,直接使用best.pt即可。

【2】預測推理

預測指令碼如下:

from ultralytics import YOLO
# Load a model
model = YOLO('best.pt')  # pretrained YOLOv8n model
 
# Run batched inference on a list of images
results = model(['1 (1).jpg', '1 (2).jpg', '1 (3).jpg'])  # return a list of Results objects
 
# Process results list
for result in results:
    boxes = result.boxes  # Boxes object for bounding box outputs
    masks = result.masks  # Masks object for segmentation masks outputs
    keypoints = result.keypoints  # Keypoints object for pose outputs
    probs = result.probs  # Probs object for classification outputs
    result.show()  # display to screen
    result.save(filename='result.jpg')  # save to disk

預測結果:

img

img

img

快速上手YOLO:利用 Roboflow 和 Ultralytics HUB 完成模型訓練與管理(上)

https://medium.com/@andy6804tw/%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8Byolo-%E5%88%A9%E7%94%A8-roboflow-%E5%92%8C-ultralytics-hub-%E5%AE%8C%E6%88%90%E6%A8%A1%E5%9E%8B%E8%A8%93%E7%B7%B4%E8%88%87%E7%AE%A1%E7%90%86-%E4%B8%8A-37acd110a8a0

在訓練人工智慧模型時,我們通常會選擇在自己的電腦或工作站上進行。然而,許多人可能會嘗試利用雲端運算工具,例如 Colab 進行訓練。但是使用過 Colab 的人都知道,訓練過程可能會變得複雜且不易管理,且檔案分散難以整理。因此,在本文中我們將介紹如何利用 Ultralytics HUB 來協助我們進行模型訓練和管理。我們將透過數據管理平台 Roboflow,將資料匯入 Ultralytics HUB,並連動 Colab 進行模型訓練。同時 Ultralytics HUB 平台提供了視覺化圖表,方便我們觀察和分析訓練過程中的各項指標。

img

Roboflow 線上數據管理平台

Roboflow 是一個專門管理影像數據的平台,目標是幫助使用者更有效地管理和處理圖像數據。它的主要功能包括數據標註、數據清理、數據轉換和數據管理。使用者可以透過 Roboflow 快速標註圖像,進行數據增強和轉換,並輕鬆地將準備好的數據集用於模型訓練。除了數據管理之外,Roboflow 平台還提供了許多不同用戶所公開的資料集。這些資料集涵蓋了各種不同的主題和應用領域,包括物件偵測、影像分割和分類等。使用者可以透過 Roboflow 平台輕鬆地瀏覽這些資料集,找到符合自己需求的資料,並加速他們的研究和開發過程。

img

數據集功能:

  • 數據導入與導出:Roboflow支持多種數據導入和導出選項,包括CSV、COCO、Pascal VOC、YOLO、TensorFlow等。這使得從其他平台導入數據到Roboflow變得輕鬆,或導出數據以供其他工具和框架使用。
  • 數據集管理:Roboflow讓管理數據集變得輕鬆,具有版本控制、數據驗證和數據過濾等功能。還可以合併數據集、刪除重複數據等。
  • 數據增強:Roboflow包含各種數據增強技術,幫助提高模型的準確性。可以對圖像添加噪聲、模糊、裁剪、旋轉、翻轉等技術。

影像標註功能:

  • 物件檢測:使用Roboflow,可以對圖像進行物件檢測的標註,並在物件周圍畫出邊界框並為其標記類別名稱。Roboflow支持多種標註類型,包括點、線和多邊形。
  • 圖像分割:圖像分割是Roboflow支持的另一種標註任務,可以將圖像中的每個像素都標記為一個類別名稱。這對於語義分割和實例分割等任務很有用。
  • 分類:Roboflow還支持圖像分類任務,可以根據其內容將圖像分類到不同的類別中。
  • 標註工具:Roboflow包含多種標註工具,以使標註過程更快速、高效。這些工具包括自動標註,其中Roboflow根據現有標註提出標註建議,以及AI標註,其中Roboflow根據圖像內容提出標註建議。
  • 協作標註:Roboflow還包括協作功能,您可以邀請其他人標註您的數據集或審查標註。這使得在大型數據集上工作或與他人合作進行標註任務變得輕鬆。

img

Roboflow 平台的使用非常直觀且易於上手。以下是使用 Roboflow 平台的基本步驟:

  • 註冊帳號
  • 建立新專案
  • 上傳數據
  • 標記數據
  • 導出數據集

完成註冊後,請登入 Roboflow。首先,平台會要求建立一個 Workspace,然後選擇使用免費版本的 Public Plan 方案。

img

成功建立一個Workspace後才能夠在該空間中新增一個專案。

img

該平台提供了四種專案類型,分別是物件檢測(Object Detection)、分類(Classification)、實例分割(Instance Segmentation)和特徵點檢測(Keypoint Detection)。在這裡,我們選擇第一個物件檢測專案。

img

在左側工具列提供了許多數據管理選項,包括標籤的命名管理、影像上傳、指派影像標註任務、資料集匯出管理。最後 Roboflow 還提供了強大的視覺化工具和分析功能,幫助使用者了解數據集的特徵和統計訊息,使更好地維護資料品質。首先我們先將手上的資料集上傳到該專案中,上傳成功後點選 Save and Continue 按鈕。

img

接著系統會指引你下個步驟,在 Roboflow 服務中提供了三種資料標籤的流程。免費的用戶可以直接選第三個擇手動標籤選項。並且可以指派其他使用者一起對這個專案進行協同標籤的任務。

img

點選 Start Annotation 即可進入標籤頁面進行人工標籤。

img

編輯頁面有點類似修圖軟體,首先針對想要預測的物體用 bounding box 圍起來,同時每個圍好的框必須都給予一個正確的類別。

img

每個類別標籤顏色都可以對應到 bounding box 框框的顏色,讓用戶可以一眼就知道物件的相對應類別。

img

全部影像都標籤完成後即可點選 Add images to Dataset,並選擇比例自動切割訓練集、驗證集、測試集。

img

這時候就可以看到剛剛所切分的資料集在左側的 Dataset 選項中。

img

完成影像標記後,即可發布第一版本的資料集。點選左側工具列的 Generate 即可發布目前的資料集。發布過程分為以下五個步驟:

  1. 選擇數據來源: 確認要發布的影像數量以及類別個數。
  2. 切分資料集: 按照自己的偏好將數據切分為訓練集、驗證集和測試集。
  3. 影像前處理: 總共分成十一種影像的預處理方法,可依照偏好添加。
  4. 資料增強: 讓原本資料集隨機的產生不一樣的樣本使模型學到更豐富的資料,根據增強方式分成影像層級增強和邊界框層級增強。
  5. 建立與發佈

img

成功建立版本後即可點選 Export Dataset 按鈕匯出資料集。Roboflow 支援了許多不同的標註格式,例如JSON、TXT、XML等。並且可以直接將整包資料集匯出下載,或是可以直接利用 Roboflow API 的方式透過 Python 連結帳戶直接下載資料集與標籤。

img

選擇 Train with Roboflow,可以直接使用內建的自動線上建模服務,一個帳號能免費使用一次模型訓練。

如果想快速實現免費的線上無程式碼自動建模服務,可以參考下一篇文章。我們將介紹如何通過 Roboflow 與 Ultralytics HUB 進行連動,並利用 Colab 快速進行物件偵測模型的訓練。

快速上手YOLO:利用 Roboflow 和 Ultralytics HUB 完成模型訓練與管理(下)

在上一篇教學中我們已經透過數據管理平台 Roboflow 為資料集進行標籤管理。並整理好訓練用的訓練集與測試集。資料整理好之後,最後一步是透過 Roboflow 匯出資料集至 Ultralytics HUB。在匯入之前記得先註冊好 Ultralytics HUB 會員並登入。另外也可以從 Roboflow Universe 上找尋社群提供的免費公開資料集。

上一篇 快速上手YOLO:利用 Roboflow 和 Ultralytics HUB 完成模型訓練與管理(上)

img

選擇 Ultralytics HUB 進行匯出,網頁將自動轉跳到 Ultralytics HUB 網站,登入後即可在 Ultralytics HUB 管理和訓練模型。

img

成功匯入(Import) 資料集之後即可,點選 Train Model 進行模型訓練。

img

Ultralytics HUB 提供了廣泛的 YOLO 系列模型訓練平台,讓用戶可以輕鬆地訓練多種不同版本的YOLO(You Only Look Once)物件偵測模型。此平台不僅提供了豐富的訓練資源,還提供預訓練模型,使得用戶能夠更快速地開始物件偵測任務,大幅縮短了模型開發和部署的時間。Ultralytics 公司發布了 Yolov5 和 Yolov8,雖然未發表論文對技術上說明。但是這一套 No Code 系統大幅降低了對物件辨識任務的上手難度。我們可以在該平台選擇 YOLOv5 或 YOLOv8 不同大小的模型架構,並且選擇是否要使用預訓練模型。此外進階設定還能動態調整模型的超參數。

img

模型選擇好之後依據平台指示將上面三行程式碼貼到下方提供的 Colab 專案內執行即可。

img

程式會先安裝一些必要的套件,然後會連接至 Ultralytics HUB。接著,它會將在 Colab 中訓練得到的結果傳送回 Ultralytics HUB。

img

訓練過程中可以透過 Ultralytics HUB 即時地監控模型收斂情形。在本範例中訓練集共有16張影像 ,並且有四種不同類別,在 Colab Tesla T4 GPU 訓練 100 個 epoch 大約花費兩分鐘。

img

模型訓練完成後可以直接在平台上進行線上的預覽(Preview)推論,驗證模型訓練的成果。可以使用圖片上傳方式亦或是開啟電腦視訊鏡頭進行物件偵測。此外部署(Deploy)功能可以直接將模型打包匯出,例如 ONNX 格式。

img

另外平台也提供了 Ultralytics Cloud API 方法,使用者可以透過 Python 呼叫已訓練好的模型並透過 HTTP Request POST 協議進行圖片上傳並回傳辨識結果。

img

還記得 Roboflow 這個平台嗎?雖然他的 No Code 模型訓練服務要錢,但是他有免費提供自己訓練好的模型上傳至它們平台。並且依樣提供雲端API服務進行推論。有興趣的讀者可以期待下篇文章,教各位如何訓練 2024 最新的 Yolov9 並透過 Roboflow 管理模型。

img

python

利用Conda嚐鮮Python 3.10

conda create -n py310 python=3.10 -c conda-forge -y
conda activate py310

建立和管理 Python 虛擬環境

列出 Conda 環境
使用以下命令來列出所有 Conda 環境:
conda env list

創建虛擬環境
若要創建一個名為 myenv 的虛擬環境,並指定 Python 版本為 3.9,可以使用以下命令:
/home/shihyu/miniconda3/envs/python3.9/bin/python3.9 -m venv myenv


啟動虛擬環境
在創建虛擬環境後,使用以下命令來啟動它:
source myenv/bin/activate


退出虛擬環境
若要退出虛擬環境,使用以下命令:
deactivate

刪除虛擬環境
若要刪除虛擬環境,可以直接刪除環境目錄:
rm -rf myenv

再見了 pip!最佳 Python 套件管理器——Poetry 完全入門指南

前陣子工作上的專案從原先的 pip 改用 Poetry 管理 Python 套件,由於採用 Poetry 正是我的提議,所以必須身先士卒,研究 Poetry 使用上的重點與學習成本,並評估是否真有所值——講白了就是至少要利大於弊,不然會徒增團隊適應上的負擔。

拜這個機會所賜,我對 Poetry 總算有了一個較為全面的理解。

習慣以後,現在我所有的個人開發也都改用 Poetry 來管理套件及虛擬環境,對於 Poetry 這個略嫌複雜的工具(相比於 pip),上手的同時我也感受到它確實存在一些學習門檻,間接促使了本文的誕生。

有鑑於 Poetry 真的有點複雜,如果要推薦別人使用,我想還是有必要好好介紹一下。

本文除了講解如何使用 Poetry,還會先不厭其煩地闡述它所解決的痛點,若興趣不大,可以直接跳到「從零開始使用 Poetry」章節,但看完前導部分,相信能更加體會 Poetry 的必要性。

為了讓你無痛上手!這將會是一篇超過 8000 字的長文,還請多多擔待。🙏

主要目錄

供快速跳轉(桌面版用戶可和右下角的「回到最上方」搭配使用):

Poetry 是什麼?

比起 Poetry GitHub 的說明:

Poetry: Dependency Management for Python Poetry helps you declare, manage and install dependencies of Python projects, ensuring you have the right stack everywhere.

我覺得 Poetry 官網的 slogan 更加簡潔有力:

簡單來說,Poetry 類似 pip,能協助你進行套件管理(dependency management),但又比 pip 強大得多,因為它還包含了 pip 所未有的功能:

  • 虛擬環境管理
  • 套件相依性管理
  • 套件的打包與發布

其中最為關鍵的是「套件的相依性管理」,也是本文的重點,而「套件的打包與發布」與本文主題較無關係,所以不會提及。

名詞解釋:虛擬環境管理、套件管理、相依性管理

開始前,要先大致說明標題中這三者的區別,才不易混淆文中的內容。這裡的定義可能不盡準確,但至少對理解文中的表達能有所幫助。

虛擬環境管理

指的是使用內建的 venv 或 vituralenv 套件來建立及管理 Python 的虛擬環境,不同的虛擬環境間各自獨立,講白了就是指向的路徑各不相同。

套件管理、依賴管理(dependency management)

指的是使用 pip 這類的套件管理器來管理 Python 環境(未必是虛擬環境),即管理環境中所安裝的全部套件(package、dependency)及其版本。

在這個語境下,dependency 基本上就是指安裝的 package。

「套件的」相依性管理、依賴解析

這個有點難定義,它並不是一個非常通俗且有共識的名詞,我在英文中也還難找到對應的名詞。本文使用它時,主要指的是套件與套件之間的依賴關係及版本衝突管理,也就是套件的「相依性管理」。在下文提及的 Podcast 中,又稱為「依賴解析」。

所謂套件的「版本衝突」指的是單一套件被兩個以上的套件所依賴,但不同的套件對依賴的套件有著不同的最低或最高版本要求,若兩者的要求沒有「交集」,則會產生衝突而導致套件失效或無法安裝。

pip 的最大不足

大概在 2 年前就知道了 Poetry 的存在,不過那時我還沒有套件相依性管理的強烈需求,加上看起來需要一些學習成本(確實如此),所以就一直擱在一旁,直到真正體會到了 pip 的不足。

pip 是 Python 內建的套件管理工具,而它的最大罩門,就是對於「套件間的相依性管理」能力不足。尤其是在「移除」套件時的依賴解析——可以說基本沒有。這也是我提議改用 Poetry 的根本原因。

怎麼說?看完下面的例子就能明白。

pip uninstall的困境:以 Flask 為例

假設現在你的工作專案中有開發 API 的需求,經過一番研究與討論,決定使用 Flask 網頁框架來進行開發。

我們知道,很多套件都有依賴的套件,也就是使用「別人已經造好的輪子」來構成套件功能的一部分。

安裝主套件時,這些依賴套件也必須一併安裝,主套件才能正常運作,這裡的 Flask 就是如此。安裝 Flask 時,不僅會安裝單一個套件flask,還會安裝所有 Flask 的必要構成部分,如下:

❯ pip install flask
Collecting flask
  Downloading Flask-2.1.1-py3-none-any.whl (95 kB)
     |████████████████████████████████| 95 kB 993 kB/s
Collecting importlib-metadata>=3.6.0
  Using cached importlib_metadata-4.11.3-py3-none-any.whl (18 kB)
Collecting itsdangerous>=2.0
  Downloading itsdangerous-2.1.2-py3-none-any.whl (15 kB)
Collecting Werkzeug>=2.0
  Downloading Werkzeug-2.1.1-py3-none-any.whl (224 kB)
     |████████████████████████████████| 224 kB 2.8 MB/s
Collecting click>=8.0
  Downloading click-8.1.2-py3-none-any.whl (96 kB)
     |████████████████████████████████| 96 kB 1.9 MB/s
Collecting Jinja2>=3.0
  Downloading Jinja2-3.1.1-py3-none-any.whl (132 kB)
     |████████████████████████████████| 132 kB 3.7 MB/s
Collecting zipp>=0.5
  Using cached zipp-3.7.0-py3-none-any.whl (5.3 kB)
Collecting MarkupSafe>=2.0
  Downloading MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl (13 kB)
Installing collected packages: zipp, MarkupSafe, Werkzeug, Jinja2, itsdangerous, importlib-metadata, click, flask
Successfully installed Jinja2-3.1.1 MarkupSafe-2.1.1 Werkzeug-2.1.1 click-8.1.2 flask-2.1.1 importlib-metadata-4.11.3 itsdangerous-2.1.2 zipp-3.7.0

從上可知,pip install flask還會一併安裝importlib-metadataitsdangerous等 7 個依賴套件,實際上總共安裝了 8 個套件!

pip 在「安裝」套件時的相依性管理還是可以的,這並不難,因為套件的依賴要求都寫在安裝檔裡了,根本不需要「解析」。


附帶一提,這 8 個套件包括flask,除了importlib-metadatazipp外,其餘 6 個實際上都是 Flask 團隊自行開發的套件

但是並不是隻有 Flask 框架會使用(依賴)這些套件。

比如其中的 Click 就是一個廣泛使用的命令製作工具。套件官網是這麼介紹的:

Click is a Python package for creating beautiful command line interfaces in a composable way with as little code as necessary.

別的套件也可能依賴click來提供命令列的功能,換句話說,主套件的依賴套件也可能被其他第三方套件所依賴、使用。


好,一切都很美好,就這樣一年過去,團隊決定改用火紅的 FastAPI 取代 Flask 來實作專案的 API,作為 API 的主要開發人員,你興高採列地安裝了 FastAPI,更新了所有程式碼,最後要移除 Flask,這時問題就來了。

安裝 Flask 的時候,只需要pip install flask,pip 就會幫你一併安裝所有依賴套件。現在要移除它,也只要pip uninstall flask就可以了嗎?

很遺憾,答案是否定的

pip 的致命缺陷:缺乏移除套件時的依賴解析(相依性管理)

僅執行pip uninstall flask的話,pip 就真的只會幫你移除flask這個套件本身而已。那剩下的、再也用不到的套件怎麼辦?你只能一個一個手動移除!

但你千萬不要真的嘗試手動移除依賴套件!——因為你無法確定這些依賴套件是否同時被別的套件所依賴

pip 手動移除依賴套件的潛在風險:以 Flask + Black 為例

繼續以 Flask 為例,還記得其中一個依賴套件是click,如前所述,它是一個協助製作命令列界面的工具。

假設專案中同時也使用 Black 這個 formatter 進行程式碼風格管理(沒錯!我現在個人開發也都改用 Black 取代 yapf 了),Black 是一個可以透過 CLI 執行的工具,很巧的,它也是使用click來實作命令列界面。

可想而知,移除 Flask 時,如果你同時把click也跟著一併移除,會發生什麼樣的悲劇——你的 Black 壞了。

簡言之,直接 pip 手動移除依賴套件存在下列兩大疑慮,不建議輕易嘗試:

一、無法確定想移除的套件還有多少依賴套件

正常而言,你不會去注意安裝時總共一併安裝了多少依賴套件。雖然有pip show這類的指令可以大概知曉套件的依賴,但這指令只會顯示「直接依賴套件」而不會顯示「依賴套件的依賴」,所以列出來的結果未必準確:

❯ pip show flask
Name: Flask
Version: 2.1.1
Summary: A simple framework for building complex web applications.
Home-page: https://palletsprojects.com/p/flask
Author: Armin Ronacher
Author-email: armin.ronacher@active-4.com
License: BSD-3-Clause
Location: /Users/kyo/.pyenv/versions/3.8.12/envs/test/lib/python3.8/site-packages
Requires: importlib-metadata, Werkzeug, click, Jinja2, itsdangerous
Required-by:

可以看到,Requires:只顯示了 5 個依賴套件,因為剩下的 2 個(zippmarkupsafe)是「依賴的依賴」,在更下層,並未顯示。

二、即使確定所有依賴套件,也無法確定這些套件是否還被其他套件所依賴

好繞口啊!上述的click例子就是解釋這個困境。

小結:pip 只適合小型專案或「只新增不移除」套件的專案

以前我的個人或工作上的專案往往規模不大,pip 就真的只負責新增,鮮少需要考慮移除套件的情況,所以缺少移除套件時的依賴解析,似乎也沒什麼大問題。

但稍具模規的專案往往就需要考慮套件的退場,以維持開發及部署環境的簡潔,尤其在使用容器化部署時,過多不必要的套件會徒增 image 的肥大,產生額外的成本與浪費。

然而透過上面的例子可知,僅靠 pip 想要乾淨移除過時的套件,且不影響既有的套件,簡直是不可能的任務!所以我們需要有完整套件依賴解析、相依性管理的套件管理器。

pip 替代方案選擇

因為 pip 存在這樣的致命弱點,所以很早就有相關的方案提出想要解決它,最知名的莫過於 Pipenv

關於 pip 的前世今生,以及為何它難以演化成理想的、可以完美管理套件相依性的版本,可以參考〈告別 Anaconda:在 macOS 上使用 pyenv + pyenv-virtualenv 建立 Python 開發環境〉中推薦過的單集 Podcast:《捕蛇者說》Ep 15. 和 PyPA 的成員聊聊 Python 開發工作流

從 Podcast 網頁「時間節點」目錄中可知,該集對 Python 的虛擬環境與套件管理機制及相關工具,有著非常廣泛的討論,十分精彩,強力推薦!(為了寫這篇又聽了第 3 次)

img

Pipenv vs Poetry

講到需要有充分「套件相依性管理」功能的套件管理器,你基本上也只能從 Pipenv 和 Poetry 兩者之中二擇一了。

如果是在兩年前,這個選擇難題恐怕不容易回答,而且 Pipenv 會有較大的機率勝出,但兩年後的今天,我建議你毫不猶豫地選擇 Poetry。

我選擇 Poetry 的第一個理由

第一個理由:不要選擇 Pipenv。

乍看之下有點鬧,但卻不失為一個具體的理由,因為當你搜尋「python poetry」關鍵字的時候,那些教你怎麼使用 Poetry 的文章往往也會一併提及為何不選擇 Pipenv。

以下兩篇有著較為完整的說明,請容我直接引用。

Python - 取代 Pipenv 的新套件管理器 Poetry〉:

Pipenv 雖然強大,卻也暴露出了一些問題如 Lock 過慢、Windows 支援性差、對 PyPI 套件打包的友善度差…等更多其他問題,甚至有越來越多人表明 不要使用 Pipenvpipenv 的凋零與替代方案 poetry 等。

同時 Pipenv 的社群維護狀況也越來越差,有許多的 PR 都沒有被 Release,導致許多貢獻者抱怨,甚至有人發出了該篇 If this project is dead, just tell us issue 想知道是否專案已經不在維護。

相比 Pipenv,Poetry 是一個更好的選擇〉(本文作者李輝為 Flask 團隊成員):

Pipenv 描繪了一個美夢,讓我們以為 Python 也有了其他語言那樣完善的包管理器,不過這一切卻在後來者 Poetry 這裡得到了更好的實現。

這幾年 Pipenv 收獲了很多用戶,但是也暴露了很多問題。雖然 Lock 太慢、Windows 支持不好和 bug 太多的問題都已經改進了很多,但對我來說,仍然不能接受隨時更新鎖定依賴的設定,在上一篇文章《不要用 Pipenv》裡也吐槽了很多相關的問題。

兩篇的內容總結就是一句話:不要用 Pipenv。

目前 Pipenv 已經由 PyPA(同時也維護 pip 及 vituralenv)接手,上述「擺爛」的情況應該是有所好轉,不過我似乎還沒看到有什麼文章大力鼓吹或宣告 Pipenv 已經「great again」,所以個人對它的未來發展還是持保留態度。

選擇 Poetry 的第二個理由:pyproject.toml

pyproject.toml 是 PEP 518 所提出的新標準:

The build system dependencies will be stored in a file named pyproject.toml that is written in the TOML format.

雖然原意是作為套件打包的標準,但後來又有了 PEP 621,擴充定性為 Python 生態系工具的共同設定檔標準,現在已經被愈來愈多套件所支援,詳細可參考這個清單及頁面中的說明:

pyproject.toml is a new configuration file defined in PEP 518 and expanded in PEP 621. It is design to store build system requirements, but it can also store any tool configuration for your Python project, possibly replacing the need for setup.cfg or other tool-specific files.

作為規範控,我很願意追隨這個標準。

並且,Poetry 使用pyproject.toml可遠遠不止是設定檔的程度,基本上相當於 Pipenv 的Pipfile或 npm 的package.json

少了pyproject.toml,Poetry 是無法運作的。

好,漫長的前言到此結束,讓我們進入正題,開始學習上手 Poetry。


從零開始使用 Poetry

本文所有的參考資料會放在文末的「參考」一欄中,不過在此還是要特別提及主要的參考對象,總共有二:

在本文找不到你需要的內容,以上二處可能會有,所以特別提及。

另外本文主要以 macOS 和 Linux 環境來進行教學及安裝,Windows 用戶如果有無法順利安裝的情況,建議參考官方文件內容修正。相信如果有問題,應該也只會集中在安裝設定階段,本文其餘部分仍可適用。

安裝 Poetry

Poetry 和 pip、git、pyenv 等工具一樣,都是典型的命令列工具,需要先安裝才能下達指令poetry

安裝方式選擇

而 Poetry 提供了兩種安裝方式:

  1. 全域安裝至使用者的家目錄。
  2. pip 安裝至專案的 Python 環境。

個人推薦使用全域安裝,連官方文件也這麼說

因為 pip 安裝是直接安裝到專案所屬的 Python 環境裡,而且 Poetry 所依賴的套件非常多,總計超過 30 個,會嚴重影響專案環境的整潔度。文件中也警告這些依賴套件的版本可能和專案既有的版本產生衝突

Be aware that it will also install Poetry’s dependencies which might cause conflicts with other packages.

全域安裝至家目錄

所以我們就使用全域安裝吧!參考 Poetry 的 GitHub 說明

macOS / Linux:

curl -sSL https://install.python-poetry.org | python3 -

Windows:

(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -

文件表示安裝的路徑如下:

The installer installs the poetry tool to Poetry’s bin directory. This location depends on your system:

  • $HOME/.local/bin for Unix
  • %APPDATA%\Python\Scripts on Windows

以 macOS 為例,此時如果要下指令,就需要打完整路徑$HOME/.local/bin/poetry,顯然不太方便,所以我們需要設定 PATH。

設定 PATH

新增poetry指令執行檔所在的路徑至 PATH。

.zshrc.bashrc.bash_profile新增:

export PATH=$PATH:$HOME/.local/bin

存檔後重啟 shell 即可使用。直接在命令列打上poetry指令測試:

❯ poetry
Poetry version 1.1.13

USAGE
  poetry [-h] [-q] [-v [<...>]] [-V] [--ansi] [--no-ansi] [-n] <command> [<arg1>] ... [<argN>]

...

設定 alias

比起pippoetry這個指令顯然太冗長了!我們還是給它一個 alias 吧!

基於它是我非常常用的指令,我願意賦與它「單字母」alias 的特權,我使用p

alias p='poetry'

測試結果:

❯ p
Poetry version 1.1.13

USAGE
  poetry [-h] [-q] [-v [<...>]] [-V] [--ansi] [--no-ansi] [-n] <command> [<arg1>] ... [<argN>]

alias 是方便自己使用,但本文基於表達清晰考量,下面的解說原則上並不會使用 alias 表示。

初始化 Poetry 專案

為了方便解說,我們先建立一個全新的專案,名為poetry-demo

指令都很簡單,但還是建議可以一步一步跟著操作。

就像 git 專案需要初始化,Poetry 也需要,因為每一個使用了 Poetry 的專案中一定要有一個pyproject.toml。所以先來初始化,使用poetry init

mkdir poetry-demo
cd poetry-demo
poetry init

此時會跳出一連串的互動對話,協助你建立專案的資料,大部分可以直接enter跳過:

This command will guide you through creating your pyproject.toml config.

Package name [poetry-demo]:
Version [0.1.0]:
Description []:
Author [kyo <odinxp@gmail.com>, n to skip]:
License []:
Compatible Python versions [^3.8]:

Would you like to define your main dependencies interactively? (yes/no) [yes]

直到出現「Would you like to define your main dependencies interactively? (yes/no) [yes]」,我會選「no」,隨即讓你確認本次產生的toml檔內容:

Would you like to define your development dependencies interactively? (yes/no) [yes] no
Generated file

[tool.poetry]
name = "poetry-demo"
version = "0.1.0"
description = ""
authors = ["kyo <odinxp@gmail.com>"]

[tool.poetry.dependencies]
python = "^3.8"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

並詢問你「Do you confirm generation? (yes/no) [yes]」,按enter使用預設選項或回答「yes」則pyproject.toml建立完成。

此時專案目錄結構如下:

poetry-demo
└── pyproject.toml

0 directories, 1 file

管理 Poetry 虛擬環境

我覺得學習 Poetry 的第一道關卡,就是它對於虛擬環境的管理。

強制虛擬環境

Poetry 預設上(可透過poetry config修改)會強制套件都要安裝在虛擬環境中,以免汙染全域,所以它整合了vitrualenv

在執行poetry add、install等指令時,Poetry 都會自動檢查是否正在使用虛擬環境:

  • 如果是,則會直接安裝套件至當前的虛擬環境。
  • 如果否,則會自行幫你建立一個獨立的虛擬環境,再進行套件安裝。

容易混淆的虛擬環境

Poetry 直接整合的虛擬環境管理算是立意良善,相當於把pip+venv的功能整合在一起,但如此也帶來一定的複雜度,尤其在你已經自行使用了venvvitrualenvpyenv-vitrualenvconda來管理虛擬環境的情況下!

沒錯,Python 的虛擬環境管理就是這麼麻煩。

個人建議,對新手而言,於 Poetry 的專案中,一律使用 Poetry 來管理虛擬環境即可。

以指令建立虛擬環境

使用指令poetry env use python

❯ poetry env use python
Creating virtualenv poetry-demo-IEWSZKSE-py3.8 in /Users/kyo/Library/Caches/pypoetry/virtualenvs
Using virtualenv: /Users/kyo/Library/Caches/pypoetry/virtualenvs/poetry-demo-IEWSZKSE-py3.8

重點說明:

  • Poetry 原則上會使用目前的 Python 版本來建立虛擬環境,這取決於python在你的 PATH 是連結到哪個版本,也可以明示為python3python3.8,前提是 PATH 中確實存在這些連結。
  • Poetry 會統一將虛擬環境建立在「特定目錄」下,本例中是/Users/kyo/Library/Caches/pypoetry/virtualenvs
  • 虛擬環境的命名模式固定為專案名稱-亂數-Python版本

老實說我個人不是很喜歡這樣的做法,因為如此一來單一專案允許建立複數個虛擬環境(Python 3.7、 3.8、3.9 可以各來一個),彈性之餘也增加了混亂程度,且命名模式我也不喜歡,太冗長了。

既然 Poetry 管理的套件環境是高度綁定專案本身的,我更偏好venv式的做法,也就是把虛擬環境放到專案目錄內,而不是統一放在獨立的目錄下,讓虛擬環境與專案呈現直觀的一對一關係

所幸 Poetry 具備這樣的選項。

修改config,建立專案內的.venv虛擬環境

讓我們使用poetry config指令來查看 Poetry 目前幾個主要的設定,需要使用--list這個參數:

❯ poetry config --list
cache-dir = "/Users/kyo/Library/Caches/pypoetry"
experimental.new-installer = true
installer.parallel = true
virtualenvs.create = true
virtualenvs.in-project = false
virtualenvs.path = "{cache-dir}/virtualenvs"

其中virtualenvs.create = true若改成false,則可以停止 Poetry 在「偵測不到虛擬環境時會自行建立」的行為模式,但建議還是不要更動。

virtualenvs.in-project = false就是我們要修改的目標:

poetry config virtualenvs.in-project true

好,我們先把之前建立的虛擬環境刪除:

❯ poetry env remove python
Deleted virtualenv: /Users/kyo/Library/Caches/pypoetry/virtualenvs/poetry-demo-IEWSZKSE-py3.8

重新建立,看看行為有何差異:

❯ poetry env use python
Creating virtualenv poetry-demo in /Users/kyo/Documents/code/poetry-demo/.venv
Using virtualenv: /Users/kyo/Documents/code/poetry-demo/.venv

可以看出:

  • 虛擬環境的路徑改為「專案的根目錄」。
  • 名稱固定為.venv

我覺得這樣的設定更加簡潔。

啟動與退出虛擬環境

啟動虛擬環境,需移至專案目錄底下,使用指令poetry shell

❯ poetry shell
Spawning shell within /Users/kyo/Documents/code/poetry-demo/.venv
❯ . /Users/kyo/Documents/code/poetry-demo/.venv/bin/activate 

poetry shell指令會偵測當前目錄或所屬上層目錄是否存在pyproject.toml來確定所要啟動的虛擬環境,所以如果不移至專案目錄,則會出現下列錯誤:

❯ poetry shell

  RuntimeError

  Poetry could not find a pyproject.toml file in /Users/kyo/Documents/code or its parents

  at ~/Library/Application Support/pypoetry/venv/lib/python3.8/site-packages/poetry/core/factory.py:369 in locate
      365│             if poetry_file.exists():
      366│                 return poetry_file
      367│
      368│         else:
    → 369│             raise RuntimeError(
      370│                 "Poetry could not find a pyproject.toml file in {} or its parents".format(
      371│                     cwd
      372│                 )
      373│             )

可以看出 Poetry 的錯誤訊息非常清楚,讓你很容易知曉修正的方向,這是作為一個命令列工具的必要優點。

退出就簡單多了,只需要exit即可。

Poetry 常用指令

Poetry 是一個獨立的命令列工具,就像 pyenv,它有自己的指令,需要花費額外的心力學習。這可能是使用 Poetry 的第二道關卡。所幸和 pyenv 一樣,常用的指令就那幾個而已,所以不用擔心,下面會一一介紹。

繼續使用前面提過的 Flask 和 Black 這兩個套件來加以來示範並說明 Poetry 的優勢與和 pip 的不同之處。本文的示範就只會安裝或移除這兩個套件而已。

Poetry 新增套件

使用指令:

poetry add

相當於pip install,我們來試著安裝 Flask 看看會有什麼變化:

img

圖中可以看出 Poetry 漂亮的命令列資訊呈現,會清楚告知總共新增了幾個套件。

此時專案中的pyproject.toml也會發生變化:

...
[tool.poetry.dependencies]
python = "^3.8"
Flask = "^2.1.1"

[tool.poetry.dev-dependencies]

[build-system]
...

這裡要說明,安裝 Flask,則pyproject.toml就只會顯示Flask = "^2.1.1"這個 top-level 的 package 項目,其餘的依賴套件不會直接記錄在toml檔中。

我覺得這是一大優點,方便區分哪些是你主動安裝的主要套件,而哪些又是基於套件的依賴關係而一併安裝的依賴套件。

poetry.lock 與更新順序

除了pyproject.toml,此時專案中還會增加一個新增檔案,名為poetry.lock,它實際上就相當於 pip 中的requirements.txt,詳細記載了所有安裝的套件與版本。

當你使用poetry add指令時,Poetry 會自動依序幫你做完這三件事:

  • 更新pyproject.toml
  • 依照pyproject.toml的內容,更新poetry.lock
  • 依照poetry.lock的內容,更新虛擬環境。

換句話說,poetry.lock的內容主要是取決於pyproject.toml,但兩者並不會自己連動,一定要基於特定指令才會進行同步與更新,poetry add就是一個典型案例。

此時專案目錄結構如下:

poetry-demo
├── poetry.lock
└── pyproject.toml

0 directories, 2 files

更新 poetry.lock

當你自行修改了pyproject.toml內容,比如變更特定套件的版本與範圍(這是有可能的,尤其在手動處理版本衝突的時候),此時poetry.lock的內容與pyproject.toml出現了脫鉤,必須讓它依照新的pyproject.toml內容來自我更新,使用指令:

poetry lock

如此一來,手動修改的內容,才能確保也更新到poetry.lock,畢竟虛擬環境如果要重新建立,是基於poetry.lock的內容來安裝套件,而非pyproject.toml

再次強調,poetry.lock相當於 Poetry 的requirements.txt

列出全部套件清單 + 樹狀顯示

類似pip list,這裡使用poetry show

❯ poetry show
click              8.1.2  Composable command line interface toolkit
flask              2.1.1  A simple framework for building complex web applications.
importlib-metadata 4.11.3 Read metadata from Python packages
itsdangerous       2.1.2  Safely pass data to untrusted environments and back.
jinja2             3.1.1  A very fast and expressive template engine.
markupsafe         2.1.1  Safely add untrusted strings to HTML/XML markup.
werkzeug           2.1.1  The comprehensive WSGI web application library.
zipp               3.8.0  Backport of pathlib-compatible object wrapper for zip files

特別提醒的是,這裡的清單內容並不是來自於虛擬環境,這點和 pip 不同,而是來自於poetry.lock的內容。

而 Poetry 最為人津津樂道的就是它的樹狀顯示poetry show --tree

❯ poetry show --tree
flask 2.1.1 A simple framework for building complex web applications.
├── click >=8.0
│   └── colorama *
├── importlib-metadata >=3.6.0
│   └── zipp >=0.5
├── itsdangerous >=2.0
├── jinja2 >=3.0
│   └── markupsafe >=2.0
└── werkzeug >=2.0

讓主要套件與其依賴套件的關係層次,一目瞭然。

安裝套件至 dev-dependencies

有些套件,比如pytestflake8等等,只會在開發環境中使用,產品的部署環境並不需要,Poetry 允許你區分這兩者,將上述的套件安裝至dev-dependencies區塊,方便讓你輕鬆建立一份沒有這些套件的虛擬環境。

在此以 Black 為例,安裝方式如下:

poetry add black -D

poetry add black --dev

結果的區別顯示在pyproject.toml裡:

...
[tool.poetry.dependencies]
python = "^3.8"
Flask = "^2.1.1"

[tool.poetry.dev-dependencies]
black = "^22.3.0"
...

可以看到black被列在不同區塊:tool.poetry.dev-dependencies

然而這是記載上的差異,使用上具體的差別為何?下面會再次提及,可以理解為「輸出套件環境」上的差異。

Poetry 移除套件

使用poetry remove指令。和poetry add一樣,可以加上-D參數來移除置於開發區的套件。

而移除套件時的「依賴解析」能力,正是 Poetry 遠遠優於 pip 的主要環節,因為 pip 沒有嘛!也是為何我提議改用 Poetry 的關鍵理由——為了順利移除套件。

前面已經提過,pip 的pip uninstall只會移除你所指定的套件,而不會連同依賴套件一起移除——因為 pip 沒有「依賴解析」功能。如果貿然移除「安裝時所有一併安裝」的依賴套件,可能會造成巨大的災難,讓別的套件失去效用。

前面也舉了 Flask 和 Black 都共同依賴click這個套件的例子,在人為手動移除的情況下,你可能未曾注意 Black 也依賴了click,結果為了「徹底移除」Flask 的所有相關套件,不小心把click也移除掉了。

當然,我知道,絕大部分的真實情況是——你根本不會去移除一段時間前安裝但已不再使用的套件。


好,解釋了很多,接下來就是 Poetry 的表演了,它會幫你處理這些棘手的「套件相依性」難題,讓你輕鬆移除 Flask 而不影響 Black:

img

可以對比上面安裝 Flask 時的截圖,總共安裝了 8 個套件,但現在移除卻只有 7 個——沒錯,因為 Poetry 知道 Black 還需要click!不能移除:

❯ poetry show --tree
black 22.3.0 The uncompromising code formatter.
├── click >=8.0.0
│   └── colorama *
├── mypy-extensions >=0.4.3
├── pathspec >=0.9.0
├── platformdirs >=2
├── tomli >=1.1.0
└── typing-extensions >=3.10.0.0

一個套件直到環境中的其餘套件都不再依賴它,Poetry 才會安心讓它被移除。

輸出 Poetry 虛擬環境的 requirements.txt

理論上,全面改用 Poetry 後,專案中是不需要存在requirements.txt,因為它的角色已經完全被poetry.lock所取代。

但事實是,你還是很可能需要它,甚至還需要隨著poetry.lock同步更新它的內容!至少對我而言就是如此,我在 Docker 部署環境中並不使用 Poetry,所以我需要一份完全等價於poetry.lockrequirements.txt用於 Docker 部署。

如果你想說,那我就在 Poetry 的虛擬環境下,使用以往熟悉的指令pip freeze > requirements.txt,來產生一份不就好了?我本來也是這麼想的。但實際的產出卻是如此(目前 poetry-demo 專案僅剩下 Black):

black @ file:///Users/kyo/Library/Caches/pypoetry/artifacts/11/4c/fc/cd6d885e9f5be135b161e365b11312cff5920d7574c8446833d7a9b1a3/black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl
click @ file:///Users/kyo/Library/Caches/pypoetry/artifacts/f0/23/09/b13d61d1fa8b3cd7c26f67505638d55002e7105849de4c4432c28e1c0d/click-8.1.2-py3-none-any.whl
mypy-extensions @ file:///Users/kyo/Library/Caches/pypoetry/artifacts/b6/a0/b0/a5dc9acd6fd12aba308634f21bb7cf0571448f20848797d7ecb327aa12/mypy_extensions-0.4.3-py2.py3-none-any.whl
...

這呈現好像不是我們以前熟悉的那種:

black==22.3.0
click==8.1.2
mypy_extensions==0.4.3
...

沒錯,只要是使用poetry add安裝的套件,在pip freeze就會變成這樣。此時想輸出類似requirements.txt的樣式,需要使用poetry export

預設輸出會有 hash 值,不想納入則要加上參數去除。現在我都是用以下指令來輸出:

poetry export -f requirements.txt -o requirements.txt --without-hashes

我們再看一下輸出結果,雖然不盡相同,但也相去不遠了…嗎?等等,怎麼是空白?


因為poetry export預設只會輸出toml中的[tool.poetry.dependencies]區塊的套件!還記得上面我們把 Black 安裝到[tool.poetry.dev-dependencies]了嗎?

顯然 Poetry 認為你的 export 需求基本上就為了部署,並不需要開發區的套件。這倒是沒錯,不過基於演示需求,我們必須輸出[tool.poetry.dev-dependencies]的套件,才能看到 Black。

加上—-dev參數即可:

poetry export -f requirements.txt -o requirements.txt --without-hashes --dev

輸出的requirements.txt內容:

black==22.3.0; python_full_version >= "3.6.2"
click==8.1.2; python_version >= "3.7" and python_full_version >= "3.6.2"
colorama==0.4.4; python_version >= "3.7" and python_full_version >= "3.6.2" and platform_system == "Windows"
...

雖然長得有點不一樣,但這個檔案確實是可以pip install的。

從這裡也可以看出前面提及的「區分套件安裝區塊」的價值了——有些時候並不需要輸出開發專用套件。

poetry export所有參數用法與說明,請參考文件

此時專案目錄結構如下:

poetry-demo
├── poetry.lock
├── pyproject.toml
└── requirements.txt

0 directories, 3 files

小結:Poetry 常用指令清單

算來算去,Poetry 的常用指令主要有下面幾個:

  • poetry add
  • poetry remove
  • poetry export
  • poetry env use
  • poetry shell
  • poetry show
  • poetry init
  • poetry install

其中一半以上,單一專案可能只會用個一兩次而已,比如initinstallenv use,實際上需要學習的指令並不多。

那麼,只要知曉這些指令,就可以順利運用 Poetry 了嗎?可能是,也可能否,所以我下面還會再補充 Poetry 的常見使用情境與操作方式,讓你接納 Poetry 的阻力可以進一步下降!

Poetry 常見使用情境與操作 QA

這部分會以「使用場景」的角度切入,介紹 Poetry 應用情境與操作說明,還包括一些自問自答,如下:

  1. 新增專案並使用 Poetry
  2. 現有專案改用 Poetry
  3. 在別臺主機回復專案狀態
  4. 我想要重建虛擬環境
  5. 為什麼我不在 Docker 環境中使用 Poetry?
  6. 我可以使用自己習慣的 vituralenv 嗎?

一、新增專案並使用 Poetry

這是最理想的狀態,沒有過去的「包袱」,可謂是最能輕鬆採用 Poetry 的情境。

使用順序不外乎是:

  1. poetry init:初始化,建立pyproject.toml
  2. poetry env use python:建立專案虛擬環境並使用。
  3. poetry shell:進入專案但虛擬環境還未啟動,以這個指令啟動。如果使用本指令時虛擬環境還不存在或已移除,則會直接自動幫你建立虛擬環境並使用。
  4. poetry add:新增套件,必要使用-D參數新增至 dev 區塊。
  5. poetry remove:移除套件,若是移除 dev 區塊的套件,需要加上-D參數。

這部分和前面內容沒有差別,因為前面內容就是以全新專案作為基礎。

二、現有專案改用 Poetry

這是極為常見的需求,但並沒有很正式的做法,因為不存在poetry import之類的指令。

首先要考量的就是:要怎麼把requirements.txt的所有項目加到pyproject.toml中呢?經過一番 Google,基本上只能土法煉鋼

cat requirements.txt | xargs poetry add

在這個過程是有可能遇到問題的,因為 Poetry 對套件的版本衝突比較敏感,所以在requirements.txt能正常安裝的項目,在上述指令的過程中可能會出錯。

那怎麼辦?只能照著錯誤訊息去修正requirements.txt中的套件版本。

並且,這個 import 做法實在是不得已,因為我們最早介紹pyproject.toml時有提到,原則上它只會記載「主套件」,但這個做法相當於把requirements.txt中的所有套件都當作主套件add了!——畢竟requirements.txt沒有能力區分主套案與依賴套件,都是「一視同仁」地列出。

如此做法讓專案的套件失去了主從之分,日後想要移除主套件時,就需要比較多心力去分辨主從,比如使用poetry show --tree去一一檢視,終究是麻煩的事。

完成轉換後,保險起見,重建一個虛擬環境會比較合適。

三、在別臺主機回復專案狀態

這也是非常常見的需求。

第一步當然是git clone專案,此時專案中已經有 Poetry 所需的必要資訊了——也就是pyproject.tomlpoetry.lock

你還缺少的僅僅是虛擬環境。如果是全新的主機,則還得先安裝、設定好 Poetry。

移至專案目錄底下,然後依序操作:

  1. poetry env use python:建立專案虛擬環境並使用。如果你懶得打這麼長的指令,直接poetry shell也是可以。
  2. poetry install:因為是舊專案,不再需要init,直接從poetry.lock安裝套件!使用的就是這個指令,類似npm install

四、我想要重建虛擬環境

在使用專案內虛擬環境方案,也就是.venv的前提下,想要刪除這個虛擬環境並加以重建,也不需要使用poetry env remove python指令了,因為會出錯。

還有更簡單暴力的方式,是什麼呢?當然是直接刪除.venv資料夾即可。

然後再poetry env use pythonpoetry shell建一個新的就好。

五、為什麼我不在 Docker 環境中使用 Poetry?

因為啟動容器後需要先安裝 Poetry 到全域,或打包一個帶有 Poetry 的 image,兩者都會增加新的藕合與依賴,我覺得並不妥當。

所幸 Poetry 依舊可以輸出requirements.txt,Docker 部署環境就繼續使用舊方案即可,而且 Poetry 本來主要就是用於「開發」時的套件管理,對部署差別不大。

六、我可以使用自己習慣的 vituralenv 嗎?

當然可以。

我本來也繼續使用pyenvvituralenv,但兩者有時候也是會小小打架,後來還是索性用 Poetry 的虛擬環境就好。一個專案對應一個虛擬環境,我認為還是比較簡潔的做法。


結語

使用 Poetry 來管理專案的套件與虛擬環境,雖然需要一定的學習成本,但帶來的效益還是相當可觀的,尤其在你希望能乾淨且安心地移除套件的時候。

別再猶豫,從今天起,加入 Poetry 的行列吧!

參考

教你用Python搭建gRPC服務

前言

前陣子剛好有個要使用 gRPC 的機會,同事看了一下官網的 Tutorial 覺得一時之間有點迷路,所以就寫了一份比較簡單的 gRPC Tutorial for Python,應該可以讓需要的人更快入門。

protocol buffers 簡介

在介紹 gRPC 之前先來講講 protocol buffers。我們在不同 service 之間做訊息溝通的時候,最常使用的也許是透過 json 來做傳遞,不過在使用 json 上面可能會遇到以下幾個問題:

  1. json 的 serialize/de-serialize 速度太慢
  2. 透過 json 傳資料的結構不夠清楚,得靠 API 文件搞定
  3. 用 json 傳資料的 size 太大

protocol buffers 便是 Google 為瞭解決以上問題而生,它可以透過一個 .proto 檔案,在各語言生出相對應的檔案使用。目前支援的程式語言很多,有 Python、golang、js、java 等等。

grpc 簡介

有了 protocol buffers 之後,google 更進一步的推出了 gRPC。透過 gRPC,我們可以在 .proto 檔案當中也一併定義好 service,讓遠端使用的 client 可以如同呼叫本地的 library 一樣使用,不用再自己處理 routing 、連線等等的問題。

img

上圖是從 gRPC 官網上面截下來的圖,可以看到 gRPC Server 是由 C++ 撰寫,client 則分別是 Java 以及 Ruby,Server 跟 Client 端則是透過 protocol buffers 來做傳遞。

開發 gRPC 流程

接下來讓我們來用 Python 開發一個最簡單的 gRPC service。分別有以下四個部分:

  1. 環境安裝 – 最基本的套件安裝
  2. .proto 檔案 – 定義 protocol buffers 還有 service
  3. Server – server 的 code
  4. Client – client 呼叫 server 的 code

最後完成的程式碼會放在 https://github.com/daikeren/gprc_tutorial

環境安裝以及設定

首先我們要安裝相對應的套件,這邊我們用 pipenv 來作為環境管理工具。輸入以下指令安裝 grpcio 以及 grpcio-tools。

pipenv install grpcio grpcio-tools

接著,我們先來定義我們 service 的功能,我們這邊為了 demo,我們這個 service 做的事情很簡單,只是將傳遞進來的 name 前面加上 “Hello” 再回傳回去。這邊當然是可以改成任何我們想要 Python Function。

# hello.py
def hello(name):
    return f"Hello {name}"

.proto 檔案

接下來,我們創建一個 hello.proto 檔案,裡面描述了我們要使用的 message 以及 service

syntax = "proto3";

message HelloRequest {
    string value = 1;
}

message HelloResponse {
    string value = 1;
}

service Hello {
    rpc Hello(HelloRequest) returns (HelloResponse) {}
}

這邊的 message 代表了我們要跟 gRPC 傳遞的參數、型別,service 則是敘述了 service name 以及傳入、傳回的參數。之後我們會透過 gprc_tools 來讀取 .proto 檔案產生相對應的 Python class。這邊可以看到我們定義的 message 以及 service 如下:

  • message: 這邊定義了兩種 message,分別是 HelloRequest 以及 HelloResponse,裡面都只有一個叫做 value 的欄位,形態是 string
  • service: 這邊定義了一個 service,裡面只有一個叫做 Hello 的 rpc 傳入值是 HelloRequest,傳回值則是 HelloResponse

接下來,我們輸入以下指令為 Python 產生 gRPC 的 class

pipenv run python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. hello.proto

你會看到多出了兩個檔案

  • hello_pb2.py: 定義了相對應的 message class
  • hello_pb2_grpc.py: 定義了相對應的 service class

Server

有了 hello_pb2.py 以及 hello_pb2_grpc.py,我們就可以開始實作我們的 gRPC server,詳細的程式碼如下:

# server.py
from concurrent import futures
import time

import grpc 
import hello_pb2
import hello_pb2_grpc

import hello

# 創建一個 HelloServicer,要繼承自 hello_pb2_grpc.HelloServicer
class HelloServicer(hello_pb2_grpc.HelloServicer):

# 由於我們 service 定義了 Hello 這個 rpc,所以要實作 Hello 這個 method
    def Hello(self, request, context):

# response 是個 HelloResponse 形態的 message
        response = hello_pb2.HelloResponse()
        response.value = hello.hello(request.value)
        return response


def serve():
# 創建一個 gRPC server
	server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

# 利用 add_HelloServicer_to_server 這個 method 把上面定義的 HelloServicer 加到 server 當中
	hello_pb2_grpc.add_HelloServicer_to_server(HelloServicer(), server)

# 讓 server 跑在 port 50051 中
	server.add_insecure_port('[::]:50051')
	server.start()
	try:
		while True:
			time.sleep(86400)
	except KeyboardInterrupt:
		server.stop(0)


if __name__ == '__main__':
	serve()

接著我們可以輸入以下指令啟動 gRPC server

pipenv run python server.py

Client

client side 的 code 很單純,就是建立一個 channel 連線到 gRPC server,再用 stub 來呼叫。

# client.py 

import grpc

import hello_pb2
import hello_pb2_grpc

# 連接到 localhost:50051
channel = grpc.insecure_channel('localhost:50051')

# 創建一個 stub (gRPC client)
stub = hello_pb2_grpc.HelloStub(channel)

# 創建一個 HelloRequest 丟到 stub 去
request = hello_pb2.HelloRequest(value="World")

# 呼叫 Hello service,回傳 HelloResponse
response = stub.Hello(request)

print(response.value)

因為我們已經在 localhost 跑起來 server 了,執行以下的 client 程式碼:

pipenv run python client.py
Hello World

小結

看到這邊應該可以理解一個簡單的 gRPC 該怎麼實作,相信以這為基礎應該可以很容易擴展出未來更多的 gRPC 應用。

參考

  • https://www.icoding.co/2020/07/grpc-tutorial-for-python

別再用 print 來 Debug 啦!來用 Python Debugger 吧!

前言

這幾年開發 Python 下來,發現不少人對於 debugger 其實還是有點陌生的。大部分在做除錯的時候,還是會用 print 或是 Python Logging 來印出程式的變數,或是理解目前程式碼進行的流程,藉此來確認自己程式的行為。在看完這篇文章之後,讀者可以學會如何透過 Python Debugger 來取代原本的 debugging 行為,你將會發現你的 debug 會更加的有效率。

對於剛開始接觸 Python 的人來說,最常使用的 debug tool 應該就是 print 了。隨著對於 Python 越來越瞭解,可能會透過 Python 的 logging module,透過像是 logging.debug 之類的 code 來印出 debug message. 這樣看似沒問題,但是往往我們用 print 大法來 debug 的流程是這樣子的:

  1. 印出某個變數的值 var1
  2. 發現 var1 的變數跟預期不太一樣,但是 var1 的值又 depends on var2
  3. 印出 var2 的值,重跑一次程式
  4. 發現 var2 的變數跟預期不太一樣,但是 var2 的值又 depends on var3 + var4
  5. 持續下去直到找到問題為止

這來回幾次的過程當中,你會先花費大量的時間印出所有你想要的值,再從這些蛛絲馬跡當中尋找到你想要的資訊,推斷問題的所在。如果你的程式執行起來很方便就算了,但是當你的程式執行起來有些麻煩或是有些久的時候,你就會耗費大量的時間在不斷的修改程式上面。而 debugger 可以幫助你解決這個問題。

當你使用 debugger 的時候,你的除錯行為會變成如下:

  1. 在你懷疑出問題的地方設定中斷點
  2. 執行程式,遇到中斷點的時候停下來
  3. 透過 debugger 的指令,來觀察你想要知道的變數值,或是一步一步的執行你的程式看看問題在哪裡
  4. Bug 解掉了!移除中斷點

pdb 基本指令

Python 就有內建 debugger pdb,要使用 pdb 很簡單,有三種方式:

  1. 不用修改原始程式碼的作法
python3 -m pdb file.py
  1. 在需要插入中斷點的程式碼中插入 breakpoint() function (Python 3.7 之後支援)

Example.

def bug_here(a):
	breakpoint()
  b = a + 3
	return b
  1. 在需要插入中斷點的程式碼插入 import pdb;pdb.set_trace()

Example.

def bug_here(a):
	import pdb;pdb.set_trace()
  b = a + 3
	return b

當你看到在 terminal 下面看到出現 (Pdb) 出現,就代表你成功的使用了 pdb.

以下是些比較常用的 pdb 指令:

  • b(reak) – 添加 breakpoint
  • p – 印出變數值
  • l(ist) – 印出目前所在 function/frame上下 11 行的程式碼
  • ll (longlist) – 印出目前所在 function/frame 的所有程式碼
  • 執行指令
    • s(tep) – 執行下一行程式碼,遇到執行 function 的時候,會進入 function 當中
    • n(ext) – 執行下一行程式碼,遇到執行 function 的時候,不會進入 function 中。
    • r(eturn) – 執行程式直到 function return
    • c(ontinue) – 持續執行程式碼直到遇到下一個中斷點
    • unt(il) – 持續執行程式直到遇到某一行
  • whatis – 印出 expression 的型別
  • interact – 啟動一個 Python 的 interpreter
  • w(here) – 印出 stack track 狀態
  • q(uit) – 離開 pdb

其他關於 pdb 的指令可以參考 pdb 官方文件有很清楚的說明。

debugger 進階

Python 內建的 pdb 已經可以滿足不少基本需求,在瞭解 pdb 的基本之後,再介紹以下兩種會讓你除錯生產力更加提升的方法。

ipdb

對於使用過 Python 一段時間的人,應該都會對 IPython 印象深刻。當中提供的 autocomplete, syntax highlight 等功能都會讓我們生產力提升不少。ipdb 就是一個可以增強原本 pdb 功能,為 pdb 帶來跟 IPython 一樣的體驗。

要使用 ipdb,首先先安裝

pip install ipdb

我們有幾種方式可以啟動 ipdb

  • python -m ipdb file.py
  • 在想要插入中斷點的地方輸入
import ipdb
ipdb.set_trace()
  • 同樣利用 Python 3.7 支援的 breakpoint(),並且在啟動的時候多設定環境變數
PYTHONBREAKPOINT=ipdb.set_trace

透過 IDE 來做 debug

如果你是 IDE 的使用者,那麼目前的 IDE 都有做了很好的整合,官方的說明文件或是網路上的 tutorial 已經很多了,以下附上一些 reference。

結論

學會使用 debugger 之後會讓你的生產力提升不少,當你下次在用 print 大法的時候,別忘記還有 debugger 可以用!

神奇又美好的 Decorator ,嗷嗚!

【導言】

由於 Python 的基本語法過於簡潔,所以大部分的設計與技巧都會為了強化其架構的依賴性與開發性,而讓 Python 的語法變得比原先的繁複。

這次要介紹一個少數「化繁為簡」的語法 — — Decorator !

Decorator 本身的概念非常簡單,但簡單語法與使用背後還有一個小魔王就是 Closure!不過 Closure 這個部份較為複雜,不適合一併放入這篇文章,所以會在之後的文章內介紹其原理!今天大家就著重在 Decorator 如何使用,以及 Decorator 使用時機吧

【本文章節】

  • 【壹、什麼是 Decorator ?!】
  • 【貳、Decorator 的 Syntax Candy — @小老鼠】
  • 【參、Decorator 的有序性】
  • 【肆、Decorator 如何帶參數 ?】
  • 【伍、Decorator 也可以是 Class !?】

【開發環境與建議先備知識】

OS Ubuntu 16.04

Python 3.6

Required Knowledge

  • First Class Function (一級函數)
  • 熟悉 Python function 概念

【壹、什麼是 Decorator ?!】

長話不如直接看一下範例 code :

def print_func_name(func):
    def wrap():
        print("Now use function '{}'".format(func.__name__))
        func()
    return wrap


def dog_bark():
    print("Bark !!!")


def cat_miaow():
    print("Miaow ~~~")


if __name__ == "__main__":
    print_func_name(dog_bark)()
    # > Now use function 'dog_bark'
    # > Bark !!!

    print_func_name(cat_miaow)()
    # > Now use function 'cat_miaow'
    # > Miaow ~~~

sample-1 https://gist.github.com/JackInTaiwan/8779504fbcbd0d8420e9996fab3c8641

以上的範例可以看到我們有兩個主要的 functions: dog_bark()cat_miaow() 要執行,但兩個 functions 都有一個共同要做的事情,都想要先 print 出自己的 function name,所以對於共同要做的事情我們抽出來用 function print_func_name(func) 來完成。

邏輯是這樣的, print_func_name(func) 會把傳入的 function 再利用一個我們命為 warp() 的內部 function「加工修飾加上一些我們要的功能」,然後在用 return wrap 吐出修飾過的 function wrap ,如此就完成「修飾」的任務啦!最後記得在 print_func_name(dog_bark)print_func_name(cat_miaow) 只會 return function 本身,所以要在後面加上 () 來 call function 喔!!

額外要注意,**在 Python 中,function 的地位和 C++、Java等不同,是屬於 ”First-class Citizen” (一等公民),故稱作 ”First-class function” (一級函數、頭等函數)。**簡單來說,就是 function 也可以當成參數傳遞並執行。另外,JavaScript 等語言也都是採用 First-class Function 的概念喔!

以上的範例,基本上是一個「藉由抽出相同或相似邏輯來簡化」的簡單到不行的範例(當然現實世界中的例子可複雜得想放棄…)。但這麼簡單的一件事情,我們就稱 function print_func_name(func) 是一個 「Decorator」(裝飾器)!

【貳、Decorator 的 Syntax Candy — @小老鼠】

在上一節中明示了什麼叫做 Decorator,這裡就要介紹它的 syntax candy — @小老鼠!

這邊簡單快速補充一下,syntax candy(語法糖、語法糖衣)就是讓語法簡化的語法,可能原先要寫數十行的 code,若該語言有提供對應的 syntax candy,很可能寫個幾行或是寫個符號上去,就可以輕鬆完成了。這在多數語言中都是極度常見的語法!

補充完畢。老師,範例請下(音樂聲起):

def print_func_name(func):
    def warp():
        print("Now use function '{}'".format(func.__name__))
        func()
    return warp


@print_func_name
def dog_bark():
    print("Bark !!!")


@print_func_name
def cat_miaow():
    print("Miaow ~~~")


if __name__ == "__main__":
    dog_bark()
    # > Now use function 'dog_bark'
    # > Bark !!!

    cat_miaow()
    # > Now use function 'cat_miaow'
    # > Miaow ~~~

sample-2 https://gist.github.com/JackInTaiwan/8c27ec000a5ab6f8f4ad124f9b5d9f5e

上面的範例中,我們在兩個主要的 functions 前面加上 decorator @print_func_name ,如此就發揮了 syntax candy 化簡語法的功效,用更簡單的語法來完成一模一樣的事情囉!

或許有沒有使用 syntax candy 對於這兩份範例 code 差不了幾行,但這是因為範例 code 太簡略,而且 function 都只被 call 過一次,所以才會感受不出來。syntax candy 是真實讓人愛不釋手的!

其實對於大部分的讀者來說,最先接觸到 Decorator 的很可能是它的 syntax candy,而不是在上一節【壹、什麼是 Decorator ?!】範例 code 中的形式。不過這地方不需要拘泥於人們口中所說的 decorator 是指原先的形式,還是它的 syntax candy,因為本質真的都一樣啦!所以本篇文章除非特別強調,否則都是指 syntax candy 形式的 decorator 。

【參、Decorator 的有序性】

如果之前有閱讀過 Python進階技巧 (2) — Static/Class/Abstract Methods之實現 的讀者,可能會記得(應該會有人記得的對吧?!)我提醒過 decorator 彼此間是有順序關係的,要額外注意!

來,老師下音樂,哦不,是下範例:

def print_func_name(func):
    def warp_1():
        print("Now use function '{}'".format(func.__name__))
        func()
    return warp_1


def print_time(func):
    import time
    def warp_2():
        print("Now the Unix time is {}".format(int(time.time())))
        func()
    return warp_2


@print_func_name
@print_time
def dog_bark():
    print("Bark !!!")



if __name__ == "__main__":
    dog_bark()
    # > Now use function 'warp_2'
    # > Now the Unix time is 1541239747
    # > Bark !!!

sample-3 https://gist.github.com/JackInTaiwan/15ebaa7abe6312ae12215bf56b6f2d5f

這裡會有一咪咪的複雜,大家要專心跟著 code 的順序看過一遍喔!

decorators 多層的話是採 ”recursive” 的方式處理,如果一個 function 有兩個以上的 decorators ,邏輯上則會先合併「最靠近」的 decorator 吐出新的 function 再由上面一個的 decorator 吃進去!

所以 dog_bark() 會先被 @print_time 吃進去,然後吐出一個叫做 wrap_2 的function,而這個 warp_2 function 又會被 @print_func_name 吃進去,吐出一個叫做 wrap_1 的 function。

所以最後執行的結果順序是先 print Now use function 'wrap_2' 再 print Now the Unix time is 1541239747 。而且你會發現由於最外層的 @print_func_name 真正吃進去的 function 是已經被 @print_time 修飾過的 function,所以 print 的是Now use function 'wrap_2' 而不是 Now use function 'dog_bark' !!

再來一個範例,檢驗大家是不是有跟上了~建議大家看完 code 先在心裡寫好答案再看結果。

下一段範例:

def print_func_name(func):
    def warp_1():
        print("Now use function '{}'".format(func.__name__))
        func()
    return warp_1


def print_time(func):
    import time
    def warp_2():
        print("Now the Unix time is {}".format(int(time.time())))
        func()
    return warp_2


@print_func_name
@print_time
def dog_bark():
    print("Bark !!!")


@print_time
@print_func_name
def cat_miaow():
    print("Miaow !!!")


if __name__ == "__main__":
    dog_bark()
    # > Now use function 'warp_2'
    # > Now the Unix time is 1541239747
    # > Bark !!!

    cat_miaow()
    # > Now the Unix time is 1541239747
    # > Now use function 'cat_miaow'
    # > Miaow !!!

sample-4 https://gist.github.com/JackInTaiwan/41ded88d4c8c13c53c56f4455534e71b

和上一個範例(sample-3 )相同,只是多了 cat_miaow() 而已。這次 cat_miaow() 上的 decorators 順序和 dog_bark() 的順序是相反的。

範例結果顯示,call cat_miaow() 會先 print Now the Unix time is 1541239747 再 print Now use function'cat_miaow' 最後才 print 主要 function 的 Miaow !!!

如果結果和你所想都完全符合,那恭喜你順利瞭解 decorator 的順序性問題惹!!!

【肆、Decorator 如何帶參數 ?】

除了上面最簡單的用法之外,還可以在 decorator 處傳入參數,非常靈活好用!

請看以下範例:

import time



def print_func_name(time):
    def decorator(func):
        def wrap():
            print("Now use function '{}'".format(func.__name__))
            print("Now Unix time is {}.".format(int(time)))
            func()
        return wrap
    return decorator


@print_func_name(time=(time.time()))
def dog_bark():
    print("Bark !!!")



if __name__ == "__main__":
    dog_bark()
    # > Now use function 'dog_bark'
    # > Now Unix time is 1639491313.
    # > Bark !!!

sample-5 https://gist.github.com/JackInTaiwan/3fda507a8803d4d30a9a0f13df834f9c

從上面的範例可以知道要讓 decorator 傳入參數,只需要改成 @print_func_name(param=param_variable) 形式即可。此處可用 arguments 的形式也可以用 key arguments 的形式傳入參數。

值得注意的是,這種 decorator 帶參的寫法:function 內還有 function,且呈現 recursive 的對稱形式。這種形式就和 “Closure” 有關,之後的文章會詳細解說,這邊先不多加說明。第一層 def print_func_name(time) 是用來解析 decorator 傳入的參數的,第二層 def decorator(func) 是吃進主要要修飾的function,和前面的範例一樣。

所以這個寫法的結論是:把原本的 code 外面多加一層用來傳入 decorator 的參數。

【伍、Decorator 也可以是 Class !?】

Decorator 除了有 function decorator ,也有 class decorator!畢竟 function 和 class 在 Python 裡頭都屬於 objects 呀!

再勞煩客倌看一回範例:

class Dog:
    def __init__(self, func):
        self.age = 10
        self.talent = func

    def bark(self):
        print("Bark !!!")

@Dog
def dog_can_pee():
    print("I can pee very hard......")



if __name__ == "__main__":
    dog = dog_can_pee

    print(dog.age)
    # > 10

    dog.bark()
    # > Bark !!!

    dog.talent()
    # > I can pee very hard......

sample-6 https://gist.github.com/JackInTaiwan/6f70279c19ed337a58c875f8f2f75cae

由上述範例可以得知,當我們的 decorator 是一個 class decorator 時,要傳入的 function 主體 dog_can_pee 就會從 class 裡頭的 __init__ initializer 被吃進去,然後執行你想操作的動作:在這個例子裡,我將傳入的 function dog_can_pee 以 assignself.talent 的方式宣告為 class 的 instance method。

這是一個非常重要、靈活而優雅的技巧,將 function **dog_can_pee** 「封裝」到 class **Dog** 的一種寫法。

延續這個例子,再稍微完整一點的示範,為什麼這個寫法非常優雅:

class Dog:
    def __init__(self, func):
        self.talent = func

    def bark(self):
        print("Bark !!!")


@Dog
def dog_can_pee():
    print("I can pee very hard......")


@Dog
def dog_can_jump():
    print("I can jump uselessly QQQ")


@Dog
def dog_can_poo():
    print("I can poo like a super pooping machine!")



if __name__ == "__main__":
    dog_1 = dog_can_pee
    dog_1.talent()
    # > I can pee very hard......

    dog_2 = dog_can_jump
    dog_2.talent()
    # > I can jump uselessly QQQ

    dog_3 = dog_can_poo
    dog_3.talent()
    # > I can poo like a super pooping machine!

sample-7 https://gist.github.com/JackInTaiwan/4557776e4d04a398258d19b5bab4ef08

這個例子透過使用 class decorator 把不同的 function 封裝到這個原本的 class裡頭了。所以 dog_1, dog_2dog_3 這三隻狗明明都是 class Dog 會的才藝卻都不同!

達到同樣效果的寫法有很多種,其中一種是利用 class 繼承的方式達成,不過如果在此處使用 class 繼承可能會過於冗餘、臃種且擴充性低,用簡潔的 decorator 反而有簡單、重複率低且擴充高的優點!

【結語】

Decorator 被大量廣泛的使用在各方 library/package 中,具有幾個最主要的優點:

  • 靈活度高
  • 易讀性高
  • 協助封裝效果好
  • 程式碼重複率低/簡潔度高

有於篇幅有限,只能提供非常簡單的例子讓大家小酌一下,decorator 如果大家願意多花點時間結合各種寫法,相信大家一定會更能感受除了上面簡單的範例以外,它帶來的各種優點!

另外, decorator 中會使用到 closure ,這是一個非常重要的概念,會在下一篇文章中解說,大家稍待一會兒!

【飯後餐點】

最後附上一些延伸相關資料。

如果你也喜歡我們的文章,幫我們動動手部肌肉,按下掌聲Clap,讓我們有動力繼續煮下一頓料理!

理解 Python 裝飾器看這一篇就夠了

講 Python 裝飾器前,我想先舉個例子,雖有點汙,但跟裝飾器這個話題很貼切。

每個人都有的內褲主要功能是用來遮羞,但是到了冬天它沒法為我們防風禦寒,咋辦?我們想到的一個辦法就是把內褲改造一下,讓它變得更厚更長,這樣一來,它不僅有遮羞功能,還能提供保暖,不過有個問題,這個內褲被我們改造成了長褲後,雖然還有遮羞功能,但本質上它不再是一條真正的內褲了。於是聰明的人們發明長褲,在不影響內褲的前提下,直接把長褲套在了內褲外面,這樣內褲還是內褲,有了長褲後寶寶再也不冷了。裝飾器就像我們這裡說的長褲,在不影響內褲作用的前提下,給我們的身子提供了保暖的功效。

談裝飾器前,還要先要明白一件事,Python 中的函數和 Java、C++不太一樣,Python 中的函數可以像普通變量一樣當做參數傳遞給另外一個函數,例如:

def foo():
    print("foo")

def bar(func):
    func()

bar(foo)

正式回到我們的主題。裝飾器本質上是一個 Python 函數或類,它可以讓其他函數或類在不需要做任何代碼修改的前提下增加額外功能,裝飾器的返回值也是一個函數/類對象。它經常用於有切面需求的場景,比如:插入日誌、性能測試、事務處理、緩存、權限校驗等場景,裝飾器是解決這類問題的絕佳設計。有了裝飾器,我們就可以抽離出大量與函數功能本身無關的雷同代碼到裝飾器中並繼續重用。概括的講,裝飾器的作用就是為已經存在的對象添加額外的功能。

先來看一個簡單例子,雖然實際代碼可能比這復雜很多:

def foo():
    print('i am foo')

現在有一個新的需求,希望可以記錄下函數的執行日誌,於是在代碼中添加日誌代碼:

def foo():
    print('i am foo')
    logging.info("foo is running")

如果函數 bar()、bar2() 也有類似的需求,怎麼做?再寫一個 logging 在 bar 函數裡?這樣就造成大量雷同的代碼,為了減少重復寫代碼,我們可以這樣做,重新定義一個新的函數:專門處理日誌 ,日誌處理完之後再執行真正的業務代碼

def use_logging(func):
    logging.warn("%s is running" % func.__name__)
    func()

def foo():
    print('i am foo')

use_logging(foo)

這樣做邏輯上是沒問題的,功能是實現了,但是我們調用的時候不再是調用真正的業務邏輯 foo 函數,而是換成了 use_logging 函數,這就破壞了原有的代碼結構, 現在我們不得不每次都要把原來的那個 foo 函數作為參數傳遞給 use_logging 函數,那麼有沒有更好的方式的呢?當然有,答案就是裝飾器。

簡單裝飾器

def use_logging(func):

    def wrapper():
        logging.warn("%s is running" % func.__name__)
        return func()   # 把 foo 當做參數傳遞進來時,執行func()就相當於執行foo()
    return wrapper

def foo():
    print('i am foo')

foo = use_logging(foo)  # 因為裝飾器 use_logging(foo) 返回的時函數對象 wrapper,這條語句相當於  foo = wrapper
foo()                   # 執行foo()就相當於執行 wrapper()

use_logging 就是一個裝飾器,它一個普通的函數,它把執行真正業務邏輯的函數 func 包裹在其中,看起來像 foo 被 use_logging 裝飾了一樣,use_logging 返回的也是一個函數,這個函數的名字叫 wrapper。在這個例子中,函數進入和退出時 ,被稱為一個橫切面,這種編程方式被稱為面向切面的編程。

@ 語法糖

如果你接觸 Python 有一段時間了的話,想必你對 @ 符號一定不陌生了,沒錯 @ 符號就是裝飾器的語法糖,它放在函數開始定義的地方,這樣就可以省略最後一步再次賦值的操作。

def use_logging(func):

    def wrapper():
        logging.warn("%s is running" % func.__name__)
        return func()
    return wrapper

@use_logging
def foo():
    print("i am foo")

foo()

如上所示,有了 @ ,我們就可以省去foo = use_logging(foo)這一句了,直接調用 foo() 即可得到想要的結果。你們看到了沒有,foo() 函數不需要做任何修改,只需在定義的地方加上裝飾器,調用的時候還是和以前一樣,如果我們有其他的類似函數,我們可以繼續調用裝飾器來修飾函數,而不用重復修改函數或者增加新的封裝。這樣,我們就提高了程序的可重復利用性,並增加了程序的可讀性。

裝飾器在 Python 使用如此方便都要歸因於 Python 的函數能像普通的對像一樣能作為參數傳遞給其他函數,可以被賦值給其他變量,可以作為返回值,可以被定義在另外一個函數內。

*args、**kwargs

可能有人問,如果我的業務邏輯函數 foo 需要參數怎麼辦?比如:

def foo(name):
    print("i am %s" % name)

我們可以在定義 wrapper 函數的時候指定參數:

def wrapper(name):
        logging.warn("%s is running" % func.__name__)
        return func(name)
    return wrapper

這樣 foo 函數定義的參數就可以定義在 wrapper 函數中。這時,又有人要問了,如果 foo 函數接收兩個參數呢?三個參數呢?更有甚者,我可能傳很多個。當裝飾器不知道 foo 到底有多少個參數時,我們可以用 *args 來代替:

def wrapper(*args):
        logging.warn("%s is running" % func.__name__)
        return func(*args)
    return wrapper

如此一來,甭管 foo 定義了多少個參數,我都可以完整地傳遞到 func 中去。這樣就不影響 foo 的業務邏輯了。這時還有讀者會問,如果 foo 函數還定義了一些關鍵字參數呢?比如:

def foo(name, age=None, height=None):
    print("I am %s, age %s, height %s" % (name, age, height))

這時,你就可以把 wrapper 函數指定關鍵字函數:

def wrapper(*args, **kwargs):
        # args是一個數組,kwargs一個字典
        logging.warn("%s is running" % func.__name__)
        return func(*args, **kwargs)
    return wrapper

帶參數的裝飾器

裝飾器還有更大的靈活性,例如帶參數的裝飾器,在上面的裝飾器調用中,該裝飾器接收唯一的參數就是執行業務的函數 foo 。裝飾器的語法允許我們在調用時,提供其它參數,比如@decorator(a)。這樣,就為裝飾器的編寫和使用提供了更大的靈活性。比如,我們可以在裝飾器中指定日誌的等級,因為不同業務函數可能需要的日誌級別是不一樣的。

def use_logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "warn":
                logging.warn("%s is running" % func.__name__)
            elif level == "info":
                logging.info("%s is running" % func.__name__)
            return func(*args)
        return wrapper

    return decorator

@use_logging(level="warn")
def foo(name='foo'):
    print("i am %s" % name)

foo()

上面的 use_logging 是允許帶參數的裝飾器。它實際上是對原有裝飾器的一個函數封裝,並返回一個裝飾器。我們可以將它理解為一個含有參數的閉包。當我 們使用@use_logging(level="warn")調用的時候,Python 能夠發現這一層的封裝,並把參數傳遞到裝飾器的環境中。

@use_logging(level="warn")`等價於`@decorator

類裝飾器

沒錯,裝飾器不僅可以是函數,還可以是類,相比函數裝飾器,類裝飾器具有靈活度大、高內聚、封裝性等優點。使用類裝飾器主要依靠類的__call__方法,當使用 @ 形式將裝飾器附加到函數上時,就會調用此方法。

class Foo(object):
    def __init__(self, func):
        self._func = func

    def __call__(self):
        print ('class decorator runing')
        self._func()
        print ('class decorator ending')

@Foo
def bar():
    print ('bar')

bar()

functools.wraps

使用裝飾器極大地復用了代碼,但是他有一個缺點就是原函數的元信息不見了,比如函數的docstring__name__、參數列表,先看例子:

# 裝飾器
def logged(func):
    def with_logging(*args, **kwargs):
        print func.__name__      # 輸出 'with_logging'
        print func.__doc__       # 輸出 None
        return func(*args, **kwargs)
    return with_logging

# 函數
@logged
def f(x):
   """does some math"""
   return x + x * x

logged(f)

不難發現,函數 f 被with_logging取代了,當然它的docstring__name__就是變成了with_logging函數的信息了。好在我們有functools.wrapswraps本身也是一個裝飾器,它能把原函數的元信息拷貝到裝飾器裡面的 func 函數中,這使得裝飾器裡面的 func 函數也有和原函數 foo 一樣的元信息了。

from functools import wraps
def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print func.__name__      # 輸出 'f'
        print func.__doc__       # 輸出 'does some math'
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   """does some math"""
   return x + x * x

裝飾器順序

一個函數還可以同時定義多個裝飾器,比如:

@a
@b
@c
def f ():
    pass

它的執行順序是從裡到外,最先調用最裡層的裝飾器,最後調用最外層的裝飾器,它等效於

f = a(b(c(f)))

[Python] import 概念

基礎介紹

  • package (套件/包) : 資料夾,含有 __init__.py
  • module (模塊) : 檔案
  • import 的方式有兩種 : 絕對路徑 / 相對路徑
  • sys.modules : 是個 dictionary,用於存放已經 import 過的 modules
  • sys.path : 是個 list,用於搜尋 import module 的各種路徑

import 流程

雖然 import 的方式有兩種 : 絕對路徑 / 相對路徑 但是 import 的流程是相同的

import xxxModule
  1. 檢查 xxxModule 是否存在於 sys.modules
  2. 若存在,則直接從 sys.modules 取出使用即可
  3. 若不存在,則依據 import 的方式來搜尋 xxxModule.py 的檔案位置
  4. 接著生成 xxxModule
  5. 再來放入 sys.modules
  6. 最後執行 xxxModule.py 裡面的 source code (以剛生成的 xxxModule 作為 scope 來執行)

syntax 比較

# 絕對路徑
import xxxModule
from xxxModule import xxxMethod

# 相對路徑
from . import xxxModule # 同一層目錄
from .. import xxxModule # 上一層目錄
from ... import xxxModule # 上上層目錄
from .xxxModule import xxxMethod

# 錯誤寫法
import .xxxModule # . 只能出現在 from 後面 

絕對路徑

有了以上的概念後,接著我們利用範例來實際操作下 (範例下載) 為求簡單,這邊 import 的方式都先使用絕對路徑

基礎練習 1

執行 D:\hochun\example\python_absolute_import>python app1.py

# 檔案結構
python_absolute_import
│  app1.py
│
└─packageA
  │  moduleA.py
  │  __init__.py
  │
  └─packageB
      │  moduleB.py
      └─  __init__.py
# packageA/__init__.py
print('& packageA')

# packageA/moduleA.py
print('& moduleA')

# packageA/packageB/__init__.py
print('& packageB')

# packageA/packageB/moduleB.py
print('& moduleB')
# app1.py
import sys
for idx, path in enumerate(sys.path):
    print(f'sys.path[{idx}]: {path}')

print('========== phase1 ==========')
print('"packageA" in sys.modules:', 'packageA' in sys.modules)
print('"packageA.moduleA" in sys.modules:', 'packageA.moduleA' in sys.modules)
print('"packageA.packageB" in sys.modules:', 'packageA.packageB' in sys.modules)
print('"packageA.packageB.moduleB" in sys.modules:', 'packageA.packageB.moduleB' in sys.modules)

print('========== phase2 ==========')
from packageA.packageB import moduleB
print('"packageA" in sys.modules:', 'packageA' in sys.modules)
print('"packageA.moduleA" in sys.modules:', 'packageA.moduleA' in sys.modules)
print('"packageA.packageB" in sys.modules:', 'packageA.packageB' in sys.modules)
print('"packageA.packageB.moduleB" in sys.modules:', 'packageA.packageB.moduleB' in sys.modules)

print('========== phase3 ==========')
from packageA import moduleA
print('"packageA" in sys.modules:', 'packageA' in sys.modules)
print('"packageA.moduleA" in sys.modules:', 'packageA.moduleA' in sys.modules)
print('"packageA.packageB" in sys.modules:', 'packageA.packageB' in sys.modules)
print('"packageA.packageB.moduleB" in sys.modules:', 'packageA.packageB.moduleB' in sys.modules)

print('========== phase4 ==========')
import packageA
print('packageA:', packageA)

輸出

& app1.py
sys.path[0]: D:\hochun\example\python_absolute_import #sys.path[0] 是當前路徑
sys.path[1]: C:\ProgramData\Anaconda3\python38.zip
sys.path[2]: C:\ProgramData\Anaconda3\DLLs
sys.path[3]: C:\ProgramData\Anaconda3\lib
sys.path[4]: C:\ProgramData\Anaconda3
sys.path[5]: C:\ProgramData\Anaconda3\lib\site-packages
sys.path[6]: C:\ProgramData\Anaconda3\lib\site-packages\win32
sys.path[7]: C:\ProgramData\Anaconda3\lib\site-packages\win32\lib
sys.path[8]: C:\ProgramData\Anaconda3\lib\site-packages\Pythonwin
========== phase1 ==========
"packageA" in sys.modules: False
"packageA.moduleA" in sys.modules: False
"packageA.packageB" in sys.modules: False
"packageA.packageB.moduleB" in sys.modules: False
========== phase2 ==========
& packageA # from packageA.packageB import moduleB 先執行 packageA.py
& packageB # from packageA.packageB import moduleB 再執行 packageB.py
& moduleB # from packageA.packageB import moduleB 最後執行 moduleB.py
"packageA" in sys.modules: True # False 改變為 True
"packageA.moduleA" in sys.modules: False
"packageA.packageB" in sys.modules: True # False 改變為 True
"packageA.packageB.moduleB" in sys.modules: True # False 改變為 True
========== phase3 ==========
& moduleA # 由於 sys.modules 已經有了 packageA,所以不會再執行 packageA.py
"packageA" in sys.modules: True
"packageA.moduleA" in sys.modules: True # False 改變為 True
"packageA.packageB" in sys.modules: True
"packageA.packageB.moduleB" in sys.modules: True
========== phase4 ==========
packageA: <module 'packageA' from 'D:\hochun\example\python_absolute_import\packageA\__init__.py'>

說明

  • from packageA.packageB import moduleB
    
    1. 檢查 packageA / packageB / moduleB 是否存在於 sys.modules
    2. 發現沒有,所以依據 import 的方式來搜尋 packageA.py / packageB.py / moduleB.py 的檔案位置
    3. 此處用的是絕對路徑,所以會利用 sys.path 來尋找檔案位置
    4. 有看到 sys.path[0] 就是根目錄嗎 ? 就是因為這個路徑,才找的到 packageA.py / packageB.py / moduleB.py
    5. 如果在 sys.path 中都找不到的話,就會出現 ModuleNotFoundError
  • from packageA import moduleA
    
    1. 由於 packageA 已存在於 sys.modules,所以不會執行 packageA.py
    2. 但是 moduleA 還不存在於 sys.modules,所以會依據 import 的方式來搜尋 moduleA.py 的檔案位置
    3. 此處用的是絕對路徑,所以會利用 sys.path 來尋找檔案位置
  • import packageA
    
    1. 經過上面的說明,很清楚知道 import 同樣的 package or module,只要 sys.modules 中還存在,就不會執行第二次
  • print(packageA)
    
    1. 有注意到嗎 ? 輸出的結果是一個名叫 packageAmodule from __init__.py

基礎練習 2

執行 D:\hochun\example\python_absolute_import>python app2.py

# app2.py
print('& app2.py')

print('dir():', dir())

import sys
print('dir():', dir())

a = 101
print('a be loaded')

b = 102
print('b be loaded')

c = 103
print('c be loaded')

print('dir():', dir())

輸出

& app2.py
dir(): ['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
dir(): ['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'sys'] # 增加 'sys'
a be loaded
b be loaded
c be loaded
dir(): ['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b', 'c', 'sys'] # 增加 'a', 'b', 'c'

觀察

  1. import 之後,或是宣告一個變數之後,我們可以在 dir() 中看到增加的名稱

基礎練習 3

執行 D:\hochun\example\python_absolute_import>python app3_1.py

# 檔案結構
python_absolute_import
│  app3_1.py
└─ app3_2.py
# app3_1.py
print('& app3_1.py')

import sys

import app3_2

print('sys == app3_2.sys:', sys == app3_2.sys)
# app3_2.py
print('& app3_2.py')

import sys

輸出

& app3_1.py
& app3_2.py
sys == app3_2.sys: True # 兩者的 sys 是相同的

觀察

  1. 不論在哪隻 module 中, import sys 後的 sys 是指向相同的記憶體位置
  2. 所以,不論在哪隻 module 中,我們常用的 sys.modules / sys.path 也都會指向相同的記憶體位置

基礎練習 4

執行 D:\hochun\example\python_absolute_import>python app4_1.py

# 檔案結構
python_absolute_import
│  app4_1.py
└─ app4_2.py
# app4_1.py
print('& app4_1.py')

import sys

print('[in app4_1.py] "app4_1" in sys.modules:', 'app4_1' in sys.modules)
print('[in app4_1.py] "app4_2" in sys.modules:', 'app4_2' in sys.modules)

import app4_2

print('[in app4_1.py] "app4_1" in sys.modules:', 'app4_1' in sys.modules)
print('[in app4_1.py] "app4_2" in sys.modules:', 'app4_2' in sys.modules)
# app4_2.py
print('& app4_2.py')

import sys

print('[in app4_2.py] "app4_1" in sys.modules:', 'app4_1' in sys.modules)
print('[in app4_2.py] "app4_2" in sys.modules:', 'app4_2' in sys.modules)

輸出

& app4_1.py
[in app4_1.py] "app4_1" in sys.modules: False # 一開始都是 False
[in app4_1.py] "app4_2" in sys.modules: False # 一開始都是 False
& app4_2.py
[in app4_2.py] "app4_1" in sys.modules: False
[in app4_2.py] "app4_2" in sys.modules: True # False 變成 True,因為 import app4_2
[in app4_1.py] "app4_1" in sys.modules: False
[in app4_1.py] "app4_2" in sys.modules: True

觀察

  1. app4_1.py 依賴於 app4_2.py
  2. 想想看如果情況變成兩隻 module 互相依賴,那該怎麼辦 ? (別擔心,待下面範例解釋)

基礎練習 5

執行 D:\hochun\example\python_absolute_import>python app5_1.py

# 檔案結構
python_absolute_import
│  app5_1.py
└─ app5_2.py
# app5_1.py
print('& app5_1.py')

firstName = 'peter'

print('[in app5_1.py] before import app5_2.py')

# (A)
import app5_2

# (B)
# import app5_2
# print('[in app5_1.py] name:', firstName, app5_2.lastName)

# (C)
# from app5_2 import lastName
# print('[in app5_1.py] name:', firstName, lastName)
# app5_2.py
print('& app5_2.py')

print('[in app5_2.py] before import app5_1.py')

import app5_1

lastName = 'kang'

print('[in app5_2.py] after import app5_1.py')

輸出

# (A)
& app5_1.py
[in app5_1.py] before import app5_2.py
& app5_2.py
[in app5_2.py] before import app5_1.py
& app5_1.py
[in app5_1.py] before import app5_2.py
[in app5_2.py] after import app5_1.py
[in app5_2.py] name: peter kang
# (B)
& app5_1.py
[in app5_1.py] before import app5_2.py
& app5_2.py
[in app5_2.py] before import app5_1.py
& app5_1.py
[in app5_1.py] before import app5_2.py # 在這之前都與 (A) 一致
Traceback (most recent call last):
  File "app5_1.py", line 14, in <module>
    import app5_2
  File "D:\hochun\example\python_absolute_import\app5_2.py", line 5, in <module>
    import app5_1
  File "D:\hochun\example\python_absolute_import\app5_1.py", line 15, in <module>
    print('[in app5_1.py] name:', firstName, app5_2.lastName)
AttributeError: partially initialized module 'app5_2' has no attribute 'lastName'
(most likely due to a circular import)
# (C)
& app5_1.py
[in app5_1.py] before import app5_2.py
& app5_2.py
[in app5_2.py] before import app5_1.py
& app5_1.py
[in app5_1.py] before import app5_2.py # 在這之前都與 (A) 一致
Traceback (most recent call last):
  File "app5_1.py", line 18, in <module>
    from app5_2 import lastName
  File "D:\hochun\example\python_absolute_import\app5_2.py", line 5, in <module>
    import app5_1
  File "D:\hochun\example\python_absolute_import\app5_1.py", line 18, in <module>
    from app5_2 import lastName
ImportError: cannot import name 'lastName' from partially initialized module 'app5_2' (most likely due to a circular import) (D:\hochun\example\python_absolute_import\app5_2.py)

觀察

  1. app5_1.py / app5_2.py 互相依賴
  2. 依據 app5_1.py 不同的寫法 (A) / (B) / (C),輸出結果也不同
  3. (A) 不會報錯
  4. (B) 報錯 AttributeError,因為在 app5_2.py 中, lastName = 'kang' 寫在 import app5_1 之後
  5. (C) 報錯 ImportError,同理 (B)
  6. 那如果將 app5_2.py 中的 lastName = 'kang' 寫在 import app5_1 之前,是不是就不會報錯了呢 ? (留給大家 try 看看)

相對路徑

複習下,在import 流程中有提到,雖然 import 的方式有兩種,但是 import 的流程是相同的 前面學習完了 import 的流程與 import 方式之一的絕對路徑 接下來,讓我們把相對路徑也一併搞定吧 ! (範例下載)

# 檔案結構
python_relative_import
│
├─level1
│  │  __init__.py
│  │
│  ├─level2
│  │  │  app1.py
│  │  │  app2.py
│  │  │  __init__.py
│  │  │
│  │  ├─level3
│  │  │      app3.py
│  │  │      __init__.py
│  │  │
│  │  └─utils
│  │          tool.py
│  │          __init__.py
│  │
│  └─utils
│          tool.py
│          __init__.py
│
└─utils
        tool.py
        __init__.py
# utils/__init__.py
print('& [utils] __init__.py')

# utils/tool.py
print('& [utils] tool.py')
name = 'chen'

# level1/__init__.py
print('& [level1] __init__.py')

# level1/utils/__init__.py
print('& [level1/utils] __init__.py')

# level1/utils/tool.py
print('& [level1/utils] tool.py')
name = 'bob'

# level1/level2/__init__.py
print('& [level1/level2] __init__.py')

# level1/level2/utils/__init__.py
print('& [level1/level2/utils] __init__.py')

# level1/level2/utils/tool.py
print('& [level1/level2/utils] tool.py')
name = 'peter'

# level1/level2/level3/__init__.py
print('& [level1/level2/level3] __init__.py')

進階練習 1

執行 D:\hochun\example\python_relative_import\level1\level2>python app1.py

# app1.py
print('& [level1/level2] app1.py')

print('__name__:', __name__)
print('__package__:', __package__)

import sys
sys.path.append('../..') # 增加新的 path
sys.path = sys.path[1:] # 刪除 sys.path[0]

from utils.tool import name
print(name)

輸出

& [level1/level2] app1.py
__name__: __main__
__package__: None
& [utils] __init__.py
& [utils] tool.py
chen # in utils/tool.py

觀察

  1. 當下路徑為 D:\hochun\example\python_relative_import\level1\level2
  2. sys.path.append('../..'),增加上上層路徑sys.path
  3. sys.path = sys.path[1:],刪除 sys.path[0] (當層路徑)

  1. 這個範例其實還是絕對路徑,所以尋找 module 會利用 sys.path
  2. 若改為執行 D:\hochun\example\python_relative_import>python level1/level2/app1.py,則會報錯 ModuleNotFoundError: No module named 'utils',因為我們修改了 sys.path,進而造成在 sys.path 中找不到 module utils,所以才會報錯

進階練習 2-1

執行 D:\hochun\example\python_relative_import\level1\level2>python app2.py

# app2.py
print('& [level1/level2] app2.py')

import sys

print('__name__:', __name__)
print('__package__:', __package__)

from ..utils import tool # 相對路徑

輸出

& [level1/level2] app2.py
__name__: __main__ # 關鍵
__package__: None # 關鍵
Traceback (most recent call last):
  File "app2.py", line 8, in <module>
    from ..utils import tool
ImportError: attempted relative import with no known parent package

觀察

  1. 當下路徑為 D:\hochun\example\python_relative_import\level1\level2
  2. __name____main__
  3. __package__None
  4. from ..utils import tool 為 import 方式的相對路徑

  1. 若 import 方式為相對路徑,則利用的不是 sys.path,而是 __name__ / __package__
  2. 因為 __package__None,這被視為最上層路徑,所以無法再用 from ..utils import tool,即便改成 from .utils import tool 也一樣會報錯
  3. 換句話說,若 module 中有寫到相對路徑,則不能直接下 python 指令去 run 該程式,除非使用 python -m (如下)

進階練習 2-2

執行 D:\hochun\example\python_relative_import>python -m level1.level2.app2

輸出

& [level1] __init__.py
& [level1/level2] __init__.py
& [level1/level2] app2.py
__name__: __main__
__package__: level1.level2 # 關鍵,不是 None 了
& [level1/utils] __init__.py
& [level1/utils] tool.py

觀察

  1. 當下路徑為 D:\hochun\example\python_relative_import
  2. python -m 後面跟的是 level1.level2.app2 而非 level1/level2/app2.py
  3. __package__level1.level2,因為如此 import 方式的相對路徑才能做到相對的作用

經由上述解釋後,現在的你應該能說出以下兩者的差異吧 !

  1. D:\hochun\example\python_relative_import>python -m level1.level2.app2
  2. D:\hochun\example\python_relative_import>python level1/level2/app2.py

進階練習 3

# app3.py
print('& [level1/level2/level3] app3.py')

import sys

print('__name__:', __name__)
print('__package__:', __package__)

from ...utils.tool import name

print(name)

最後這個練習就讓大家動手玩玩看囉

參考文章

  1. Python 的 Import 陷阱
  2. 理解Python的 relative 和 absolute import

Python-import導入上級目錄文件

假設有如下目錄結構:

-- dir0
  | file1.py
  | file2.py
  | dir3
   | file3.py
  | dir4
   | file4.py

dir0文件夾下有file1.py、file2.py兩個文件和dir3、dir4兩個子文件夾,dir3中有file3.py文件,dir4中有file4.py文件。

1.導入同級模塊

python導入同級模塊(在同一個文件夾中的py文件)直接導入即可。

import xxx

如在file1.py中想導入file2.py,注意無需加後綴".py":

import file2
# 使用file2中函數時需加上前綴"file2.",即:
# file2.fuction_name()

2.導入下級模塊

導入下級目錄模塊也很容易,需在下級目錄中新建一個空白的__init__.py文件再導入:

from dirname import xxx

如在file1.py中想導入dir3下的file3.py,首先要在dir3中新建一個空白的__init*__*.py文件。

-- dir0
  | file1.py
  | file2.py
  | dir3
   | __init__.py
   | file3.py
  | dir4
   | file4.py

再使用如下語句:

# plan A
from dir3 import file3

或是

# plan B
import dir3.file3
# import dir3.file3 as df3

但使用第二種方式則下文需要一直帶著路徑dir3書寫,較為累贅,建議可以另起一個別名。

3.導入上級模塊

要導入上級目錄下模塊,可以使用sys.path:  

import sys 
sys.path.append("..") 
import xxx

如在file4.py中想引入import上級目錄下的file1.py:

import sys 
sys.path.append("..") 
import file1

**sys.path的作用:**當使用import語句導入模塊時,解釋器會搜索當前模塊所在目錄以及sys.path指定的路徑去找需要import的模塊,所以這裡是直接把上級目錄加到了sys.path裡。

**“..”的含義:**等同於linux裡的‘..’,表示當前工作目錄的上級目錄。實際上python中的‘.’也和linux中一致,表示當前目錄。

4.導入隔壁文件夾下的模塊

如在file4.py中想引入import在dir3目錄下的file3.py。

這其實是前面兩個操作的組合,其思路本質上是將上級目錄加到sys.path裡,再按照對下級目錄模塊的方式導入。

同樣需要被引文件夾也就是dir3下有空的__init__.py文件。

-- dir
  | file1.py
  | file2.py
  | dir3
   | __init__.py
   | file3.py
  | dir4
   | file4.py

同時也要將上級目錄加到sys.path裡:

import sys
sys.path.append("..")
from dir3 import file3

5.常見錯誤及import原理:

在使用直接從上級目錄引入模塊的操作時:

from .. import xxx

經常會報錯:

ValueError: attempted relative import beyond top-level package

這是由於相對導入時,文件夾實質上充當的是package,也就是包的角色(比如我們常用的numpy、pandas都是包)。如果python解釋器沒有認同該文件夾是package,那麼這就是一個普通的文件夾,無法實現相對導入。

文件夾作為package需要滿足如下兩個條件:

  1. 文件夾中必須存在有__init__.py文件,可以為空。
  2. 不能作為頂層模塊來執行該文件夾中的py文件。

Multi-processing 和Multi-threading 的優缺點:

  • Multi-processing (多處理程序/多進程):
    1. 資料在彼此間傳遞變得更加複雜及花時間,因為一個 process 在作業系統的管理下是無法去存取別的 process 的 memory
    2. 適合需要 CPU 密集,像是迴圈計算
  • Multi-threading (多執行緒/多線程):
    1. 資料彼此傳遞簡單,因為多執行緒的 memory 之間是共用的,但也因此要避免會有 Race Condition 問題
    2. 適合需要 I/O 密集,像是爬蟲需要時間等待 request 回覆
import threading, logging, time
import multiprocessing


class Producer(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.stop_event = threading.Event()

    def stop(self):
        self.stop_event.set()

    def run(self):
        while not self.stop_event.is_set():
            print("Producer is working...")
            time.sleep(1)


class Consumer(multiprocessing.Process):
    def __init__(self):
        multiprocessing.Process.__init__(self)
        self.stop_event = multiprocessing.Event()

    def stop(self):
        self.stop_event.set()

    def run(self):
        while not self.stop_event.is_set():
            print("Consumer is working...")
            time.sleep(1)


def main():
    tasks = [Producer(), Consumer()]

    for t in tasks:
        t.start()

    time.sleep(3600)

    for task in tasks:
        task.stop()

    for task in tasks:
        task.join()


if __name__ == "__main__":
    logging.basicConfig(
        format="%(asctime)s.%(msecs)s:%(name)s:%(thread)d:%(levelname)s:%(process)d:%(message)s",
        level=logging.INFO,
    )
    main()


前言

出處: https://www.wongwonggoods.com/python/python-multiprocessing/

在 python 中有 thread 與 multiprocess 兩種平行處理程式的方式, 若只是單純的平行需求,我們可以使用 threading 這個模組來快速完成平行處理的方式。 但是 threading 只是透過頻繁的 CPU context-switch 的方式實現, 要真正實現多核心 CPU 的平行運算,我們需要使用 multiprocessing, 將任務指派給多個核心進行操作。

multiprocessing 在資料傳遞上,會因為需要將資料轉移至其他 CPU 上進行運算, 因此會需要考慮資料搬運的時間, 而多核心真正的實現「平行運算的功能」,當任務較為複雜時,效率一定比較好。

thread 與 multiprocess 比較

threading 重點摘要

threading 是透過 context-switch 的方式實現 也就是說,我們是透過 CPU 的不斷切換 (context-switch),實現平行的功能。 當大量使用 threading 執行平行的功能時,反而會因為大量的 context-switch, 「實現了程式平行的功能,但也因為大量的 context-switch ,使得程式執行速度更慢」。

multiprocessing 重點摘要

multiprocessing 在資料傳遞上,會因為需要將資料轉移至其他 CPU 上進行運算, 因此會需要考慮資料搬運的時間, 而多核心真正的實現「平行運算的功能」,當任務較為複雜時,效率一定比較好。

thread 與 multiprocess 比較圖

從下圖我們可以看到任務被完成的「概念」時間

  • main 1~4, main-end
  • 任務 A1, A2
  • 任務 B1, B2
  • 任務 C1, C2

請留意圖中粗線的部分: * 在 multithread 中, CPU context-switch 會額外消耗我們程式執行的時間,程式實際完成時間可能比一般的還要慢。

  • 在 multiprocess 中, 我們需要將資料轉移至其他 CPU 會額外消耗我們程式執行的時間,如果任務過於簡單,效益可能不大。

雖然示意圖中明顯感覺較快,但前提是任務夠複雜 也就是說,「任務難度執行的時間 > 資料轉移至其他 CPU 的時間效益」,不然只會更慢。

img

multiprocess 基本使用

基本的 multiprocess 使用方式,跟 thread 幾乎一樣, 如果學習過 multithread 的讀者,相信可以上手的很快。

範例程式碼 (single-multiprocess)

import multiprocessing as mp

def task(a, b):
  print('Task in the Process.')
  print(a, b)

if __name__=='__main__': # must put thread in the main
  p1 = mp.Process(target=task, args=(1,2))
  p1.start()
  p1.join()

運行結果

說明

  • p1 = mp.Process(target=task, args=(1,2))
    • 建立一個名字為 p1 的 Process,執行 task 任務,傳入參數 (1,2)
  • p1.start():啟動 p1 任務
  • p1.join():等待 p1 任務結束 (一定會等到結束才執行下一行)

多個 Process 同時平行處理,「保證」任務「結果」的順序性 (multi-process)

我們「不保證」任務執行時,「過程中」輸出的順序,但完成「結果」的順序性可用 join() 來「保證」。

範例程式碼 (multi-process)

import multiprocessing as mp

def task(num):
  print('This is Process: ', num)

if __name__=='__main__':
  num_process = 5
  process_list = []
  for i in range(num_process):
    process_list.append(mp.Process(target = task, args = (i,)))
    process_list[i].start()

  for i in range(num_process):
    process_list[i].join()

運行結果 (注意:每次執行不一定相同)

img

注意:該輸入的都有輸出,但有些順序搶先輸出了,這也代表不同核心接到任務的順序。 所以「每次執行不一定相同」。

說明

  • process_list.append(mp.Process(target = task, args = (i,)))
    • 建立 Process,存入,執行 task 任務,傳入參數 (i, )
  • process_list[i].start():啟動 process_list[i] 任務
  • process_list[i].join():等待 process_list[i] 任務結束 (一定會等到結束才執行下一行)

多個 Process 同時平行處理,「保證」任務「過程中」的順序性 (multi-process)

答:沒有必要

如果是為了當任務「過程中」順序有高度要求時…你可能要想想

如果真要確保「過程中」照順序來,才做下一件事情,那你用 multi-process 到底要幹嘛XDD。 直接寫就好了,不用想太多 multi-process 的事情! 「又要多核心」、「又要平行任務」、「又要保證過程中的順序」, 光是保證「保證過程中的順序」,你的 process 執行過程之間就會互相卡爆了 還要效率不如直接不要平行了吧。

所以這邊就不示範了,你可能要先想清楚:為什麼都用到 multi-process , 還需要保證任務「過程中」的順序性。(如果只是想確保執行「結果」的順序性,請見上面。)

利用 multiprocessing 模組 查看自己的CPU「有多少核心」

我們可以利用 multiprocessing 模組內建的功能, multiprocessing.cpu_count(),得到目前 cpu 的核心數量。

cpu_count = multiprocessing.cpu_count()

結合上述的程式範例,製作出「依照 CPU 核心數執行任務」的範例程式碼模板

import multiprocessing as mp

def task(num):
  print('This is cpu core: ', num)

if __name__=='__main__':
  cpu_count = mp.cpu_count()
  print("cpu_count: ", cpu_count)
  process_list = []
  for i in range(cpu_count):
    process_list.append(mp.Process(target = task, args = (i,)))
    process_list[i].start()

  for i in range(cpu_count):
    process_list[i].join()

執行結果 (依照不同電腦的 CPU 能力而有異)

img

不過這樣的感覺很不踏實對吧! 感覺都要手動指定核心數量給 Process, 能不能讓系統自動分配呢?

當然是可以的,我們會再另外一篇文章 multiprocessing pool 教學進階的使用, 使用 pool 就可以自動讓系統幫我們分配任務給多個核心, 並且與 Process 最大的不同是「pool 能夠取得結果」。

Reference

Python模塊-進程間的通信(Queue,Pipe)與數據共享(Manager)

出處: http://www.taroballz.com/2018/01/11/processing_communcation/

Introduction:

  • 進程之間互相獨立,預設為不能共享數據
  • 透過multiprocess模塊中的Pipe及Queue實現不同進程之間的通信
    • Queue(隊列):
      • 先進來的先出去,後進來的後出去
  • 透過Manager實現進程之間的數據共享

Notice:

  • 使用queue.Queue()調用的方法為線程隊列不適用於進程間的通信

Usage:

Queue :

from multiprocessing import Process,Queue

def func(q,name):                        #以參數的方式將對列物件以參數型式導入子進程          
    q.put('My Process_name is %s and put the data to the id %d queue'%(name,id(q)))
       
if __name__ == "__main__":
    q = Queue()              #於主進程創建隊列物件
    process_list=[]
    print("main queue id: %d"%id(q))
    for i in range(3):
        proc = Process(target=func,args=(q,'Process-%d'%i))
        process_list.append(proc)
        proc.start()
    print(q.get())    #往管道中取數據
    print(q.get())
    print(q.get())
    for each_process in process_list:
        each_process.join() 
  • Queue()

    參數可填入管道的長度

    • Queue(3)表示創建能存三筆資料的管道物件
  • 創建的Queue物件可放置任意數據類型

  • 通過 Queue.get() 取數據

    • put的先取出,後put的後取出
    • 要是Queue為空的情況下,還執意get的話,會堵塞到有數據可取出為止
    • 可使用Queue.get_nowait()方法,要是堵塞了會直接報錯

其結果如下

main queue id: 52342000
My Process_name is Process-1 and put the data to the id 64566512 queue
My Process_name is Process-0 and put the data to the id 43398512 queue
My Process_name is Process-2 and put the data to the id 61551856 queue

從上面執行結果可知道我們所創建的Queue物件似乎為一個copy的對象並指向不同RAM地址貌似不是對同一個隊列進行操作 但是從主進程get()的結果卻發現的確是對同一隊列的資料進行操作, 原因應為copy後的隊列內部進行了pickle的序列化及反序列化的操作

判斷Queue是否為滿(full)或空(empty)

Queue.full() #判斷管道是否為滿
Queue.empty() #判斷管道是否為空
  • 返回bool值

Pipe (類似socket通信)

from multiprocessing import Process,Pipe
import os

def func(conn):
    conn.send("Hi I'm your subprocess. My ID is %d"%os.getpid())
    print("ID %d receive main_process message: "%os.getpid(),conn.recv())
    conn.close()
       
if __name__ == "__main__":
    main_conn , sub_conn = Pipe()    #使用Pipe()函數同時建立主進程及自進程兩個通信的物件
    processlist=[]
    for i in range(2):
        proc = Process(target=func,args=(sub_conn,))
        processlist.append(proc)
        proc.start()
        print("I'm mainprocess, I receive my sub_process message: ",main_conn.recv())
        main_conn.send("Remember I'm your Master")
    for each_process in processlist:
        each_process.join()

其結果如下

I'm mainprocess, I receive my sub_process message:  Hi I'm your subprocess. My ID is 8424
ID 8424 receive main_process message:  Remember I'm your Master
I'm mainprocess, I receive my sub_process message:  Hi I'm your subprocess. My ID is 13468
ID 13468 receive main_process message:  Remember I'm your Master

Manager

from multiprocessing import Process,Manager

def func(dic,lis,n):            #對字典及列表進行操作
    dic["Process_%s"%n] = "1"
    dic['2'] = 2
    dic[0.25] = None
    lis.append(n)

if __name__ == "__main__":
    with Manager() as manager:       #創建一個Manager()的物件
        dic = manager.dict()         #透過Manager()物件創建一個空字典 此字典進程之間可以共享
        lis = manager.list(range(5)) #透過Manager()物件創建一個含0-5數字的列表 此列表進程之間可以共享
        process_list = []
        for i in range(10):
            proc = Process(target=func, args=(dic,lis,i))
            proc.start()
            process_list.append(proc)
            
        for each_process in process_list:
            each_process.join()
        print(dic)
        print(lis)

其結果為

{0.25: None, 'Process_9': '1', '2': 2, 'Process_8': '1', 'Process_4': '1', 'Process_0': '1', 'Process_7': '1', 'Process_2': '1', 'Process_6': '1', 'Process_1': '1', 'Process_3': '1', 'Process_5': '1'}
[0, 1, 2, 3, 4, 0, 1, 2, 3, 6, 5, 4, 8, 7, 9]

我們可以發現使用Manager各個進程是對同一個列表及字典進行操作

Reference:

http://www.cnblogs.com/yuanchenqi/articles/5745958.html

python中的wait和notify

這次講一下python中的wait和notify

現在假如有如下情況:

小明:小紅

小紅:在

小明:我喜歡你 小紅:對不起,你是個好人。

對於這種一問一答的方式,我們是否也可以通過加鎖來解決呢,我們看代碼。

import threading

class XiaoMing(threading.Thread):
    def __init__(self,lock):
        super().__init__(name='小明')
    
    
    def run(self):
        lock.acquire()
        print('{}:小紅'.format(self.name))
        lock.release()

        lock.acquire()
        print('{}:我喜歡你'.format(self.name))
        lock.release()

class XiaoHong(threading.Thread):
    def __init__(self,lock):
        super().__init__(name='小紅')
    
    def run(self):
        lock.acquire()
        print('{}:在'.format(self.name))
        lock.release()

        lock.acquire()
        print('{}:對不起,你是個好人'.format(self.name))
        lock.release()

if __name__ == '__main__':
    lock = threading.Lock()
    xiaoming = XiaoMing(lock)
    xiaohong = XiaoHong(lock)
    
    xiaoming.start()
    xiaohong.start()

這個時候會出現一個現象,我們發現執行的結果是

小明:小紅 小明:我喜歡你 小紅:在 小紅:對不起,你是個好人

很顯然,這跟我們想要的結果是不一樣的,那麼為什麼會導致這樣的結果呢。

原因就在於我們在小明說完小紅的時候會釋放鎖,接著小明這個線程又拿到了鎖,這個時候又繼續說了我喜歡你。這就是導致結果跟預期不一致的原因。

因此我們在這裡引出了wait和notify這兩個方法,這兩個方法屬於threading的Condition類,condition是一個條件變量,是用來控制復雜的線程之間的同步。

如果看過condition的源碼,就會發現condition實現了__enter__ 和__exit__這兩個魔術方法,因此我們可以通過with語句來使用condition這個變量。

再說一下wait和notify,wait()只有在被notify喚醒時,才會繼續往下執行。因此會有下面這樣的代碼。

import threading
from threading import Condition


class XiaoMing(threading.Thread):
    def __init__(self,condition):
        super().__init__(name='小明')
        self.condition = condition
    
    def run(self):
        with self.condition:
            print('{}:小紅'.format(self.name))
            self.condition.notify()
            self.condition.wait()

            print('{}:我喜歡你'.format(self.name))
            self.condition.notify()
            self.condition.wait()


class XiaoHong(threading.Thread):
    def __init__(self,condition):
        super().__init__(name='小紅')
        self.condition = condition

    def run(self):
        with self.condition:
            self.condition.wait()
            print('{}:在'.format(self.name))
            self.condition.notify()

            self.condition.wait()
            print('{}:對不起,你是個好人'.format(self.name))
            self.condition.notify()

if __name__ == '__main__':
    condition = threading.Condition()
    xiaoming = XiaoMing(condition)
    xiaohong = XiaoHong(condition)
    
    xiaohong.start()
    xiaoming.start()

運行上面的代碼,我們發現執行結果按照我們預期的進行了。需要注意的一點就是start的順序改了,是小紅先start,小明才start。

如果是小明先start的話,那麼小紅就會在小明notify之後才start,這樣小紅的wait就收不到小明發過來的信號了,因此會導致

程序一直卡住。

其實這個condition的源碼裡面,在初始化condition的時候,就會上一把鎖,這樣另一個線程就進不去with裡面了,

而在調用wait的時候,會先把condition時初始化的鎖釋放掉,然後再分配一把鎖到condition的等待隊列中,等待notify的喚醒。

本文主要講解生產者消費者模式,它基於執行緒之間的通訊。

生產者消費者模式是指一部分程式用於生產資料,一部分程式用於處理資料,兩部分分別放在兩個執行緒中來執行。

舉幾個例子

  • 一個程式專門往列表中新增數字,另一個程式專門提取數字進行處理,二者共同維護這樣一個列表
  • 一個程式去抓取待爬取的url,另一個程式專門解析url將資料儲存到檔案中,這相當於維護一個url佇列
  • 維護ip池,一個程式在消耗ip進行爬蟲,另一個程式看ip不夠用了就啟動開始抓取

我們可以想象到,這種情況不使用併發機制(如多執行緒)是難以實現的。如果程式線性執行,只能做到先把所有url抓取到列表中,再遍歷列表解析資料;或者解析的過程中將新抓到的url加入列表,但是列表的增添和刪減並不是同時發生的。對於更復雜的機制,執行緒程式更是難以做到,比如維護url列表,當列表長度大於100時停止填入,小於50時再啟動開始填入。

本文結構

本文思路如下

  • 首先,兩個執行緒維護同一個列表,需要使用鎖保證對資源修改時不會出錯
  • threading模組提供了Condition物件專門處理生產者消費者問題
  • 但是為了呈現由淺入深的過程,我們先用普通鎖來實現這個過程,通過考慮程式的不足,再使用Condition來解決,讓讀者更清楚Condition的用處
  • 下一步,python中的queue模組封裝了Condition的特性,為我們提供了一個方便易用的佇列結構。用queue可以讓我們不需要了解鎖是如何設定的細節
  • 執行緒安全的概念解釋
  • 這個過程其實就是執行緒之間的通訊,除了Condition,再補充一種通訊方式Event

本文分為下面幾個部分

  • Lock與Condition的對比
  • 生產者與消費者的相互等待
  • Queue
  • 執行緒安全
  • Event

Lock與Condition的對比

下面我們實現這樣一個過程

  • 維護一個整數列表integer_list,共有兩個執行緒
  • Producer類對應一個執行緒,功能:隨機產生一個整數,加入整數列表之中
  • Consumer類對應一個執行緒,功能:從整數列表中pop掉一個整數
  • 通過time.sleep來表示兩個執行緒執行速度,設定成Producer產生的速度沒有Consumer消耗的快

程式碼如下

import time
import threading
import random
class Producer(threading.Thread):
    # 產生隨機數,將其加入整數列表
    def __init__(self, lock, integer_list):
        threading.Thread.__init__(self)
        self.lock = lock
        self.integer_list = integer_list
    def run(self):
        while True: # 一直嘗試獲得鎖來新增整數
            random_integer = random.randint(0, 100)
            with self.lock:
                self.integer_list.append(random_integer)
                print('integer list add integer {}'.format(random_integer))
            time.sleep(1.2 * random.random()) # sleep隨機時間,通過乘1.2來減慢生產的速度
class Consumer(threading.Thread):
    def __init__(self, lock, integer_list):
        threading.Thread.__init__(self)
        self.lock = lock
        self.integer_list = integer_list
    def run(self):
        while True: # 一直嘗試去消耗整數
            with self.lock:
                if self.integer_list: # 只有列表中有元素才pop
                    integer = self.integer_list.pop()
                    print('integer list lose integer {}'.format(integer))
                    time.sleep(random.random())
                else:
                    print('there is no integer in the list')
def main():
    integer_list = []
    lock = threading.Lock()
    th1 = Producer(lock, integer_list)
    th2 = Consumer(lock, integer_list)
    th1.start()
    th2.start()
if __name__ == '__main__':
    main()

程式會無休止地執行下去,一個產生,另一個消耗,擷取前面一部分執行結果如下

integer list add integer 100
integer list lose integer 100
there is no integer in the list
there is no integer in the list
... 幾百行一樣的 ...
there is no integer in the list
integer list add integer 81
integer list lose integer 81
there is no integer in the list
there is no integer in the list
there is no integer in the list
......
複製程式碼

我們可以看到,整數每次產生都會被迅速消耗掉,消費者沒有東西可以處理,但是依然不停地詢問是否有東西可以處理(while True),這樣不斷地詢問會比較浪費CPU等資源(特別是詢問之後不只是print而是加入計算等)。

如果可以在第一次查詢到列表為空的時候就開始等待,直到列表不為空(收到通知而不是一遍一遍地查詢),資源開銷就可以節省很多。Condition物件就可以解決這個問題,它與一般鎖的區別在於,除了可以acquire release,還多了兩個方法wait notify,下面我們來看一下上面過程如何用Condition來實現

import time
import threading
import random
class Producer(threading.Thread):
    def __init__(self, condition, integer_list):
        threading.Thread.__init__(self)
        self.condition = condition
        self.integer_list = integer_list
    def run(self):
        while True:
            random_integer = random.randint(0, 100)
            with self.condition:
                self.integer_list.append(random_integer)
                print('integer list add integer {}'.format(random_integer))
                self.condition.notify()
            time.sleep(1.2 * random.random())
class Consumer(threading.Thread):
    def __init__(self, condition, integer_list):
        threading.Thread.__init__(self)
        self.condition = condition
        self.integer_list = integer_list
    def run(self):
        while True:
            with self.condition:
                if self.integer_list:
                    integer = self.integer_list.pop()
                    print('integer list lose integer {}'.format(integer))
                    time.sleep(random.random())
                else:
                    print('there is no integer in the list')
                    self.condition.wait()
def main():
    integer_list = []
    condition = threading.Condition()
    th1 = Producer(condition, integer_list)
    th2 = Consumer(condition, integer_list)
    th1.start()
    th2.start()
if __name__ == '__main__':
    main()

相比於LockCondition只有兩個變化

  • 在生產出整數時notify通知wait的執行緒可以繼續了
  • 消費者查詢到列表為空時呼叫wait等待通知(notify

這樣結果就井然有序

integer list add integer 7
integer list lose integer 7
there is no integer in the list
integer list add integer 98
integer list lose integer 98
there is no integer in the list
integer list add integer 84
integer list lose integer 84
.....
複製程式碼

生產者與消費者的相互等待

上面是最基本的使用,下面我們多實現一個功能:生產者一次產生三個數,在列表數量大於5的時候停止生產,小於4的時候再開始

import time
import threading
import random
class Producer(threading.Thread):
    def __init__(self, condition, integer_list):
        threading.Thread.__init__(self)
        self.condition = condition
        self.integer_list = integer_list
    def run(self):
        while True:
            with self.condition:
                if len(self.integer_list) > 5:
                    print('Producer start waiting')
                    self.condition.wait()
                else:
                    for _ in range(3):
                        self.integer_list.append(random.randint(0, 100))
                    print('now {} after add '.format(self.integer_list))
                    self.condition.notify()
            time.sleep(random.random())
class Consumer(threading.Thread):
    def __init__(self, condition, integer_list):
        threading.Thread.__init__(self)
        self.condition = condition
        self.integer_list = integer_list
    def run(self):
        while True:
            with self.condition:
                if self.integer_list:
                    integer = self.integer_list.pop()
                    print('all {} lose {}'.format(self.integer_list, integer))
                    time.sleep(random.random())
                    if len(self.integer_list) < 4:
                        self.condition.notify()
                        print("Producer don't need to wait")
                else:
                    print('there is no integer in the list')
                    self.condition.wait()
def main():
    integer_list = []
    condition = threading.Condition()
    th1 = Producer(condition, integer_list)
    th2 = Consumer(condition, integer_list)
    th1.start()
    th2.start()
if __name__ == '__main__':
    main()

可以看下面的結果體會消長過程

now [33, 94, 68] after add
all [33, 94] lose 68
Producer don't need to wait
now [33, 94, 53, 4, 95] after add
all [33, 94, 53, 4] lose 95
all [33, 94, 53] lose 4
Producer don't need to wait
now [33, 94, 53, 27, 36, 42] after add
all [33, 94, 53, 27, 36] lose 42
all [33, 94, 53, 27] lose 36
all [33, 94, 53] lose 27
Producer don't need to wait
now [33, 94, 53, 79, 30, 22] after add
all [33, 94, 53, 79, 30] lose 22
all [33, 94, 53, 79] lose 30
now [33, 94, 53, 79, 60, 17, 34] after add
all [33, 94, 53, 79, 60, 17] lose 34
all [33, 94, 53, 79, 60] lose 17
now [33, 94, 53, 79, 60, 70, 76, 21] after add
all [33, 94, 53, 79, 60, 70, 76] lose 21
Producer start waiting
all [33, 94, 53, 79, 60, 70] lose 76
all [33, 94, 53, 79, 60] lose 70
all [33, 94, 53, 79] lose 60
all [33, 94, 53] lose 79
Producer don't need to wait
all [33, 94] lose 53
Producer don't need to wait
all [33] lose 94
Producer don't need to wait
all [] lose 33
Producer don't need to wait
there is no integer in the list
now [16, 67, 23] after add
all [16, 67] lose 23
Producer don't need to wait
now [16, 67, 49, 62, 50] after add
複製程式碼

Queue

queue模組內部實現了Condition,我們可以非常方便地使用生產者消費者模式

import time
import threading
import random
from queue import Queue
class Producer(threading.Thread):
    def __init__(self, queue):
        threading.Thread.__init__(self)
        self.queue = queue
    def run(self):
        while True:
            random_integer = random.randint(0, 100)
            self.queue.put(random_integer)
            print('add {}'.format(random_integer))
            time.sleep(random.random())
class Consumer(threading.Thread):
    def __init__(self, queue):
        threading.Thread.__init__(self)
        self.queue = queue
    def run(self):
        while True:
            get_integer = self.queue.get()
            print('lose {}'.format(get_integer))
            time.sleep(random.random())
def main():
    queue = Queue()
    th1 = Producer(queue)
    th2 = Consumer(queue)
    th1.start()
    th2.start()
if __name__ == '__main__':
    main()

Queue

  • get方法會移除並賦值(相當於list中的pop),但是它在佇列為空的時候會被阻塞(wait)
  • put方法是往裡面新增值
  • 如果想設定佇列最大長度,初始化時這樣做queue = Queue(10)指定最大長度,超過這個長度就會被阻塞(wait)

使用Queue,全程不需要顯式地呼叫鎖,非常簡單易用。不過內建的queue有一個缺點在於不是可迭代物件,不能對它迴圈也不能檢視其中的值,可以通過構造一個新的類來實現,詳見這裡

下面消防之前Condition方法,用Queue實現生產者一次加3個,消費者一次消耗1個,每次都返回當前佇列內容,改寫程式碼如下

import time
import threading
import random
from queue import Queue
# 為了能檢視佇列資料,繼承Queue定義一個類
class ListQueue(Queue):
    def _init(self, maxsize):
        self.maxsize = maxsize
        self.queue = [] # 將資料儲存方式改為list
    def _put(self, item):
        self.queue.append(item)
    def _get(self):
        return self.queue.pop()
class Producer(threading.Thread):
    def __init__(self, myqueue):
        threading.Thread.__init__(self)
        self.myqueue = myqueue
    def run(self):
        while True:
            for _ in range(3): # 一個執行緒加入3個,注意:條件鎖時上在了put上而不是整個迴圈上
                self.myqueue.put(random.randint(0, 100))
            print('now {} after add '.format(self.myqueue.queue))
            time.sleep(random.random())
class Consumer(threading.Thread):
    def __init__(self, myqueue):
        threading.Thread.__init__(self)
        self.myqueue = myqueue
    def run(self):
        while True:
            get_integer = self.myqueue.get()
            print('lose {}'.format(get_integer), 'now total', self.myqueue.queue)
            time.sleep(random.random())
def main():
    queue = ListQueue(5)
    th1 = Producer(queue)
    th2 = Consumer(queue)
    th1.start()
    th2.start()
if __name__ == '__main__':
    main()

得到結果如下

now [79, 39, 64] after add
lose 64 now total [79, 39]
now [79, 39, 9, 42, 14] after add
lose 14 now total [79, 39, 9, 42]
lose 42 now total [79, 39, 9]
lose 27 now total [79, 39, 9, 78]
now [79, 39, 9, 78, 30] after add
lose 30 now total [79, 39, 9, 78]
lose 21 now total [79, 39, 9, 78]
lose 100 now total [79, 39, 9, 78]
now [79, 39, 9, 78, 90] after add
lose 90 now total [79, 39, 9, 78]
lose 72 now total [79, 39, 9, 78]
lose 5 now total [79, 39, 9, 78]
複製程式碼

上面限制佇列最大為5,有以下細節需要注意

  • 首先ListQueue類的構造:因為Queue類的原始碼中,put是呼叫了_putget呼叫_get_init也是一樣,所以我們重寫這三個方法就將資料儲存的型別和存取方式改變了。而其他部分鎖的設計都沒有變,也可以正常使用。改變之後我們就可以通過呼叫self.myqueue.queue來訪問這個列表資料
  • 輸出結果很怪異,並不是我們想要的。這是因為Queue類的原始碼中,如果佇列數量達到了maxsize,則put的操作wait,而put一次插入一個元素,所以經常插入一個等一次,迴圈無法一次執行完,而print是在插入三個之後才有的,所以很多時候其實加進去值了卻沒有在執行結果中顯示,所以結果看起來比較怪異。所以要想靈活使用還是要自己來定義鎖的位置,不能簡單依靠queue

另外,queue模組中有其他類,分別實現先進先出、先進後出、優先順序等佇列,還有一些異常等,可以參考這篇文章官網

執行緒安全

講到了Queue就提一提執行緒安全。執行緒安全其實就可以理解成執行緒同步。

官方定義是:指某個函式、函式庫在多執行緒環境中被呼叫時,能夠正確地處理多個執行緒之間的共享變數,使程式功能正確完成。

我們常常提到的說法是,某某某是執行緒安全的,比如queue.Queue是執行緒安全的,而list不是。

根本原因在於前者實現了鎖原語,而後者沒有。

原語指由若干個機器指令構成的完成某種特定功能的一段程式,具有不可分割性;即原語的執行必須是連續的,在執行過程中不允許被中斷。

queue.Queue是執行緒安全的,即指對他進行寫入和提取的操作不會被中斷而導致錯誤,這也是在實現生產者消費者模式時,使用List就要特意去加鎖,而用這個佇列就不用的原因。

Event

EventCondition的區別在於:Condition = Event + Lock,所以Event非常簡單,只是一個沒有帶鎖的Condition,也是滿足一定條件等待或者執行,這裡不想說很多,只舉一個簡單的例子來看一下

import threading
import time
class MyThread(threading.Thread):
    def __init__(self, event):
        threading.Thread.__init__(self)
        self.event = event
    def run(self):
        print('first')
        self.event.wait()
        print('after wait')
event = threading.Event()
MyThread(event).start()
print('before set')
time.sleep(1)
event.set()
複製程式碼

可以看到結果

first
before set
複製程式碼

先出現,1s之後才出現

after wait
複製程式碼

Loguru:更為優雅、簡潔的 Python 日誌管理模塊

img

在 Python 開發中涉及到日誌記錄,我們或許通常會想到內置標準庫 —— logging 。雖然 logging 庫採用的是模塊化設計,可以設置不同的 handler 來進行組合,但是在配置上較為繁瑣。同時在多線程或多進程的場景下,若不進行特殊處理還會導致日誌記錄會出現異常。

本文將介紹一個十分優雅、簡潔的日誌記錄第三方庫—— loguru ,我們可以通過導入其封裝的 logger 類的實例,即可直接進行調用。

安裝

使用 pip 安裝即可,Python 3 版本的安裝如下:

pip3 install loguru

基本使用

我們直接通過導入 loguru 封裝好的 logger 類的實例化對象,不需要手動創建 logger,直接進行調用不同級別的日誌輸出方法。我們先用一個示例感受下:

from loguru import logger

logger.debug('This is debug information')
logger.info('This is info information')
logger.warning('This is warn information')
logger.error('This is error information')

在 IDE 或終端運行時會發現,loguru 在輸出的不同級別信息時,帶上了不同的顏色,使得結果更加直觀,其中也包含了時間、級別、模塊名、行號以及日誌信息。

img

loguru 中不同日誌級別與日誌記錄方法對應關係 如下:

eIMZOjeIMZOj

loguru 配置日誌文件

logger 默認採用 sys.stderr 標準錯誤輸出將日誌輸出到控制檯中,假如想要將日誌同時輸出到其他的位置,比如日誌文件,此時我們只需要使用一行代碼即可實現。

例如,將日誌信息輸出到 2021-3-28.log 文件中,可以這麼寫:

from loguru import logger

logger.add("E:/PythonCode/MOC/log_2021-3-28.log",rotation="500MB", encoding="utf-8", enqueue=True, retention="10 days")

logger.info('This is info information')

如上,loguru 直接通過 add() 方法,完成了日誌文件的配置。

日誌內容的字符串格式化

loguru 在輸出 日誌的時候,還提供了非常靈活的字符串格式化輸出日誌的功能,如下:

import platform
from loguru import logger

rounded_value = round(0.345, 2)

trace= logger.add('2021-3-28.log')

logger.info('If you are using Python {version}, prefer {feature} of course!', version=platform.python_version(), feature='f-strings')

# 執行上述代碼,輸出結果為
2021-03-28 13:43:26.232 | INFO     | __main__:<module>:9 - If you are using Python 3.7.6, prefer f-strings of course!

loguru 日誌常用參數配置解析

  • sink:可以傳入一個 file 對象(file-like object),或一個 str 字符串或者 pathlib.Path 對象,或一個方法(coroutine function),或 logging 模塊的 Handler(logging.Handler)。
  • level (int or str, optional) :應將已記錄消息發送到接收器的最低嚴重級別。
  • format (str or callable, optional) :格式化模塊,在發送到接收器之前,使用模板對記錄的消息進行格式化。
  • filter (callable, str or dict, optional) :用於決定每個記錄的消息是否應該發送到接收器。
  • colorize (bool, optional) – 是否應將格式化消息中包含的顏色標記轉換為用於終端著色的 Ansi 代碼,或以其他方式剝離。如果 None,根據水槽是否為 TTY 自動作出選擇。
  • serialize (bool, optional) :在發送到接收器之前,記錄的消息及其記錄是否應該首先轉換為 JSON 字符串。
  • backtrace (bool, optional) :格式化的異常跟蹤是否應該向上擴展,超出捕獲點,以顯示生成錯誤的完整堆棧跟蹤。
  • diagnose (bool, optional) :異常跟蹤是否應該顯示變量值以簡化調試。在生產中,這應該設置為 “False”,以避免洩漏敏感數據。
  • enqueue (bool, optional) :要記錄的消息在到達接收器之前是否應該首先通過多進程安全隊列。當通過多個進程將日誌記錄到文件中時,這是非常有用的。這還具有使日誌調用非阻塞的優點。
  • catch (bool, optional) :是否應該自動捕獲接收器處理日誌消息時發生的錯誤。如果 True 上顯示異常消息 sys.stderr。但是,異常不會傳播到調用者,從而防止應用程序崩潰。

如果當接收器(sink)是文件路徑( pathlib.Path )時,可以應用下列參數,同時 add() 會返回與所添加的接收器相關聯的標識符:

  • rotation:分隔日誌文件,何時關閉當前日誌文件並啟動一個新文件的條件,; 例如,"500 MB"、"0.5 GB"、"1 month 2 weeks"、"10h"、"monthly"、"18:00"、"sunday"、"monday at 18:00"、"06:15"
  • retention (str, int, datetime.timedelta or callable, optional) ,可配置舊日誌的最長保留時間,例如,"1 week, 3 days"、"2 months"
  • compression (str or callable, optional) :日誌文件在關閉時應轉換為的壓縮或歸檔格式,例如,"gz"、"bz2"、"xz"、"lzma"、"tar"、"tar.gz"、"tar.bz2"、"tar.xz"、"zip"
  • delay (bool, optional):是否應該在配置了接收器之後立即創建文件,或者延遲到第一個記錄的消息。默認為'False'。
  • mode (str, optional) :與內置 open() 函數一樣的打開模式。默認為'"a"(以附加模式打開文件)。
  • buffering (int, optional) :內置 open() 函數的緩衝策略,它默認為 1(行緩衝文件)。
  • encoding (str, optional) :文件編碼與內置的'open()'函數相同。如果'None',它默認為'locale.getpreferredencoding() 。

loguru 日誌常用方式

停止日誌記錄到文件中

add 方法 添加 sink 之後我們也可以對其進行刪除, 刪除的時候根據剛剛 add 方法返回的 id 進行刪除即可,還原到標準輸出。如下:

from loguru import logger

trace= logger.add('2021-3-28.log')
logger.error('This is error information')

logger.remove(trace)
logger.warning('This is warn information')

控制檯輸出如下:

2021-03-28 13:38:22.995 | ERROR    | __main__:<module>:7 - This is error information

2021-03-28 13:38:22.996 | WARNING  | __main__:<module>:11 - This is warn information

日誌文件 2021-3-28.log 內容如下:

2021-03-28 13:38:22.995 | ERROR    | __main__:<module>:7 - This is error information

將 sink 對象移除之後,在這之後的內容不會再輸出到日誌文件中。

只輸出到文本,不在 console 輸出

通過 logger.remove(handler_id=None) 刪除以前添加的處理程序,並停止向其接收器發送日誌。然後通過 add 添加輸出日誌文件,即可 實現 只輸出到文本,不在 console 輸出,如下:

from loguru import logger
# 清除之前的設置
logger.remove(handler_id=None) 

trace= logger.add('2021-3-28.log')

logger.error('This is error information')
logger.warning('This is warn information')

filter 配置日誌過濾規則

如下,我們通過實現自定義方法 error_only,判斷日誌級別,當日誌級別為 ERROR,返回 TRUE,我們在 add 方法設置 filter 參數時,設置為 error_only,即可過濾掉 ERROR 以外的所有日誌 。

from loguru import logger

def error_only(record):
    """
    error 日誌 判斷 
    Args:
        record: 

    Returns: 若日誌級別為ERROR, 輸出TRUE

    """
    return record["level"].name == "ERROR"

# ERROR以外級別日誌被過濾掉
logger.add("2021-3-28.log", filter=error_only)

logger.error('This is error information')
logger.warning('This is warn information')

在 2021-3-28.log 日誌中,我們可以看到僅記錄了 ERROR 級別日誌。

2021-03-28 17:01:33.267 | ERROR    | __main__:<module>:11 - This is error information

format 配置日誌記錄格式化模板

from loguru import logger

def format_log():
    """

    Returns:

    """
    trace = logger.add('2021-3-28.log', format="{time:YYYY-MM-DD HH:mm:ss} {level} From {module}.{function} : {message}")

    logger.warning('This is warn information')

if __name__ == '__main__':
    format_log()

如下,我們可以看到在 2021-3-28.log 日誌文件中,如 "{time:YYYY-MM-DD HH:mm:ss} {level} From {module}.{function} : {message}" 格式模板進行記錄:

# 2021-3-28.log
2021-03-28 14:46:25 WARNING From 2021-3-28.format_log : This is warn information

其它的格式化模板屬性 如下:

6fMec96fMec9

通過 extra bind() 添加額外屬性來為結構化日誌提供更多屬性信息,如下:

from loguru import logger

def format_log():
    """

    Returns:

    """
    trace = logger.add('2021-3-28.log', format="{time:YYYY-MM-DD HH:mm:ss} {extra[ip]}  {extra[username]} {level} From {module}.{function} : {message}")
    
    extra_logger = logger.bind(ip="192.168.0.1", user)
    extra_logger.info('This is info information')
    extra_logger.bind(user)

    extra_logger.warning('This is warn information')

if __name__ == '__main__':
    format_log()

如下,我們可以看到在 2021-3-28.log 日誌文件中,看到日誌按上述模板記錄,如下:

2021-03-28 16:27:11 192.168.0.1  張三 INFO From 2021-3-28.format_log : This is info information
2021-03-28 16:27:11 192.168.0.1  李四 ERROR From 2021-3-28.format_log : This is error information
2021-03-28 16:27:11 192.168.0.1  張三 WARNING From 2021-3-28.format_log : This is warn information

level 配置日誌最低日誌級別

from loguru import logger

trace = logger.add('2021-3-29.log', level='ERROR')

rotation 配置日誌滾動記錄的機制

我們想週期性的創建日誌文件,或者按照文件大小自動分隔日誌文件,我們可以直接使用 add 方法的 rotation 參數進行配置。

例如,每 200MB 創建一個日誌文件,避免每個 log 文件過大,如下:

from loguru import logger

trace = logger.add('2021-3-28.log', rotation="200 MB")

例如,每天 6 點 創建一個日誌文件,如下:

from loguru import logger

trace = logger.add('2021-3-28.log', rotation='06:00')

例如,每隔 2 周創建一個 日誌文件,如下:

from loguru import logger

trace = logger.add('2021-3-28.log', rotation='2 week')

retention 配置日誌保留機制

通常,一些久遠的日誌文件,需要週期性的去清除,避免日誌堆積,浪費存儲空間。我們可以通過 add 方法的 retention 參數可以配置日誌的最長保留時間。

例如,設置日誌文件最長保留 7 天,如下:

from loguru import logger

trace = logger.add('2021-3-28.log', retention='7 days')

compression 配置日誌壓縮格式

loguru 還可以配置文件的壓縮格式,比如使用 zip 文件格式保存,示例如下:

from loguru import logger

trace = logger.add('2021-3-28.log', compression='zip')

serialize 日誌序列化

如果我們希望輸出類似於 Json-line 格式的結構化日誌,我們可以通過 serialize 參數,將日誌信息序列化的 json 格式寫入 log 文件,最後可以將日誌文件導入類似於 MongoDB、ElasticSearch 中用作後續的日誌分析,代碼示例如下:

from loguru import logger
import platform

rounded_value = round(0.345, 2)

trace= logger.add('2021-3-28.log', serialize=True)

logger.info('If you are using Python {version}, prefer {feature} of course!', version=platform.python_version(), feature='f-strings')

在 2021-3-28.log 日誌文件,我們可以看到每條日誌信息都被序列化後存在日誌文件中,如下:

{
    "text": "2021-03-28 13:44:17.104 | INFO     | __main__:<module>:9 - If you are using Python 3.7.6, prefer f-strings of course!\n",
    "record": {
        "elapsed": {
            "repr": "0:00:00.010911",
            "seconds": 0.010911
        },
        "exception": null,
        "extra": {
            "version": "3.7.6",
            "feature": "f-strings"
        },
        "file": {
            "name": "2021-3-28.py",
            "path": "F:/code/MOC/2021-3-28.py"
        },
        "function": "<module>",
        "level": {
            "icon": "\u2139\ufe0f",
            "name": "INFO",
            "no": 20
        },
        "line": 9,
        "message": "If you are using Python 3.7.6, prefer f-strings of course!",
        "module": "2021-3-28",
        "name": "__main__",
        "process": {
            "id": 22604,
            "name": "MainProcess"
        },
        "thread": {
            "id": 25696,
            "name": "MainThread"
        },
        "time": {
            "repr": "2021-03-28 13:44:17.104522+08:00",
            "timestamp": 1616910257.104522
        }
    }
}

Traceback 記錄(異常追溯)

loguru 集成了一個名為 better_exceptions 的庫,不僅能夠將異常和錯誤記錄,並且還能對異常進行追溯,如下,我們通過在遍歷列表的過程中刪除列表元素,以觸發 IndexError 異常,

通過 catch 裝飾器的方式實現異常捕獲,代碼示例如下:

from loguru import logger

trace= logger.add('2021-3-28.log')

@logger.catch
def index_error(custom_list: list):

    for index in range(len(custom_list)):
        index_value = custom_list[index]
        if custom_list[index] < 2 :
            custom_list.remove(index_value)

        print(index_value)

if __name__ == '__main__':
    index_error([1,2,3])

運行上述代碼,我們可以發現 loguru 輸出的 Traceback 日誌信息, Traceback 日誌信息中同時輸出了當時的變量值,如下:

img

在 2021-3-28.log 日誌文件中也同樣輸出了上述格式的異常追溯信息,如下。

2021-03-28 13:57:13.852 | ERROR    | __main__:<module>:16 - An error has been caught in function '<module>', process 'MainProcess' (7080), thread 'MainThread' (32280):
Traceback (most recent call last):

> File "F:/code/MOC/2021-3-28.py", line 16, in <module>
    index_error([1,2,3])
    └ <function index_error at 0x000001FEB84D0EE8>

  File "F:/code/MOC/2021-3-28.py", line 9, in index_error
    index_value = custom_list[index]
                  │           └ 2
                  └ [2, 3]

IndexError: list index out of range

同時,附上對類中的類方法和靜態方法的代碼實例,以供參考

from loguru import logger

trace = logger.add('2021-3-28.log')

class Demo:
    @logger.catch
    def index_error(self, custom_list: list):
        for index in range(len(custom_list)):
            index_value = custom_list[index]
            if custom_list[index] < 2:
                custom_list.remove(index_value)

    @staticmethod
    @logger.catch
    def index_error_static(custom_list: list):
        for index in range(len(custom_list)):
            index_value = custom_list[index]
            if custom_list[index] < 2:
                custom_list.remove(index_value)


if __name__ == '__main__':
    # Demo().index_error([1, 2, 3])
    Demo.index_error_static([1, 2, 3])

通過 logger.exception 方法也可以實現異常的捕獲與記錄:

from loguru import logger

trace = logger.add('2021-3-28.log')

def index_error(custom_list: list):
    for index in range(len(custom_list)):
        try:
            index_value = custom_list[index]
        except IndexError as  err:
            logger.exception(err)
            break

        if custom_list[index] < 2:
            custom_list.remove(index_value)


if __name__ == '__main__':
    index_error([1, 2, 3])

運行上述代碼,我們可以發現 loguru 輸出的 Traceback 日誌信息, Traceback 日誌信息中同時輸出了當時的變量值,如下:

img

  • 雖然cython非平行處理,但其可編譯python與openmp,因此cython是非常重要的套件。
  • Cython 是包含 C 資料類型的 Python。
  • Cython 是 Python:幾乎所有 Python 代碼都是合法的 Cython 程式碼。 Cython 的編譯器會轉化 Python 程式碼為 C 程式碼,這些 C 程式碼均可以調用 Python/C 的 API。
  • 一般來講,在python中使用C/C++模組兩種常見的場景是:
    • 原來的python代碼性能太差
    • 有現成的C/C++可供直接調用 *
  • pycharm IDE (professional version) 支援cython語法,建議使用。
  • cython簡單的說明,就是替python的變數加上type後再編譯成machine code,因此效能可提高不少。

imgCython編譯引用流程。

imgCython編譯流程2。

pyx/pxd檔案命名

  • cython的程式之副檔案為pyx(python extension)。

  • 而另外還有.pxd的檔案,其功能如C語言的標頭檔(header file),其包含了cython的宣告。

  • pyx可使用cimport引入pxd中的內容。

  • pxd的主要功能如下:

    • 共用C的外部宣告,或包裝已編譯的C函式庫。

    • inline C function

      cdef inline int int_min(int a, int b):
        return b if b < a else a
      

函數定義

  • cython的函式有三種定義方式:
    • def: 傳入python物件,返回python物件,直接調用
    • cdef: 傳入python物件或C/C++值,返回python物件或C/C++值,不可直接調用
    • cpdef: 以上兩者的混合

imgCython function定義可視範圍。

  • 然而使用cdef報錯不能很好的捕獲異常。你可以這樣使用:
# 這樣當該函數內部出錯時,將會返回一個0。
# (所以此時應當避免正確的情況中有返回0的可能,以避免歧義。)
cdef int divide(int x, int y) except 0:
    ...

函數參數傳遞

  • python和C/C++之間有一些自動的類型轉換:
C typesfrom python typesto python types
[unsigned] char [unsigned] short int, longint, longint
unsigned int unsigned long [unsigned] long longint, longlong
float, double, long doubleint, long, floatfloat
char*str/bytesstr/bytes
structdict
  • 如果需要檢測傳入的參數不是None的話可以加上not None來檢測
def func(x not None):
    ...

字串傳遞

  • python2.7的字串為ascii或是unicode,而python3.x之後全面使用unicode。
  • C的字串(char*)結尾是以\0為結尾。
  • C++使用string函式庫處理字串。

bytes to C string

# python: bytes to unicode
ustring = byte_string.decode('UTF-8')
// 如果C string沒有null bytes,也可用同樣方法轉換成unicode
cdef char* some_c_string = c_call_returning_a_c_string()
ustring = some_c_string.decode('UTF-8')

// 已知字串長度時,轉換會更有效率
cdef char* c_string = NULL
cdef Py_ssize_t length = 0

# get pointer and length from a C function
get_a_c_string(&c_string, &length)
ustring = c_string[:length].decode('UTF-8')

C string to bytes

py_byte_string = py_unicode_string.encode('UTF-8')

#  pointer to the byte buffer of the Python byte string
cdef char* c_string = py_byte_string

# this will not compile !
cdef char* c_string = py_unicode_string.encode('UTF-8')

# Here, the Cython compiler notices that the code takes a pointer 
# to a temporary string result that will be garbage collected after the 
# assignment. Later access to the invalidated pointer will read invalid
# memory and likely result in a segfault. 
# Cython will therefore refuse to compile this code.

for C++ string

  • 可直接將std:string的字串給python使用 ```python from libcpp.string cimport string cdef string cpp_string = py_unicode_string.encode('UTF-8')

cdef string s = string(b'abcdefg') ustring1 = s.decode('UTF-8') ustring2 = s[2:-2].decode('UTF-8')

### 自動轉換

* cython 0.19提供了兩個命令 <span class='cmd'> c_string_type</span> 與 <span class='cmd'> c_string_encoding </span>處理字串。

```python
# cython: c_string_type=unicode, c_string_encoding=utf8

cdef char* c_string = 'abcdefg'

# implicit decoding:
cdef object py_unicode_object = c_string

# explicit conversion to Python bytes:
py_bytes_object = <bytes>c_string

Hello world程式

  • hello.pyx

    print ("hello world")
    
  • setup.py

from distutils.core import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize("hello.pyx")
 )
  • 編譯cython檔案: python setup.py build_ext --inplace
    • 在linux中預設使用gcc編譯,而windows中必須安裝visual c++才可編譯。
    • 因為在編譯時指定--inplace,linux最後會生成同一資料夾中生成hello.so,而windows會生成hello.pyd。
    • 而此時可在python檔案或是shell中,使用 import hello。
  • 如果你的模組不需要額外的 C 庫活特殊的構件安裝,可使用pyximport 模組來直接讀取 .pyx 檔,而不需要編寫 setup.py 文件。 它隨同 Cython 一併發佈和安裝,你可以這樣使用它。
import pyximport; pyximport.install()
>>> import helloworld
Hello World

const修飾字

  • cython自0.18開始支援const修飾字。
  • 許多C函式庫會使用const修飾字指定不可修改的字串或陣列如下。
typedef const char specialChar;
int process_string(const char* s);
const unsigned char* look_up_cached_string(const unsigned char* key);
# cython可使用const修飾字引用C的函式庫
cdef extern from "someheader.h":
    ctypedef const char specialChar
    int process_string(const char* s)
    const unsigned char* look_up_cached_string(const unsigned char* key)

# 即使在pxd中不使用const修飾字時,仍然可引用外部C函式
cdef extern from "someheader.h":
    int process_string(char* s)   # note: looses API information!

調用C函式庫

  • 簡單來說,我們先以一個 C 標準庫中的函數為例。 你不需要向你的代碼中引入 額外的依賴,Cython 都已經幫你定義好了這些函數。所以你可以將這些函數直接 cimport 進來並使用。
    • cimport等同於C中的include
    • 你可以在 Cython 的原始程式碼包Cython/Includes/_. 中找到所有的標準 cimport 檔。這些檔保存在.pxd 檔中,這是一種標準再模組間共用 Cython 函式宣告的方法
  • 舉個例子,比如說當你想用最簡單的方法將char*類型的值轉化為一個整型值時, 你可以使用atoi() 函數,這個函數是在stdlib.h 標頭檔中定義的。
from libc.stdlib cimport atoi

# cdef: 為C函式
cdef parse_charptr_to_py_int(char* s):
    assert s is not NULL, "byte string value is NULL"
    # note: atoi() has no error detection!
    return atoi(s)
  • 使用C的數學庫
from libc.math cimport sin

cdef double f(double x):
    return sin(x*x)

動態連結(Dynamic linking)

  • 在一些類 Unix 系統(例如 linux)中,默認不提供libc math 庫。 所以除了 cimport函式宣告外,你還必須配置你的編譯器以連結共用庫m。 對於 distutils來說,在Extension()安裝變數libraries 中將其添加進來就可以了。
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize

ext_modules=[
    Extension("demo",
    sources=["demo.pyx"],
    libraries=["m"] # Unix-like specific
)
]

setup(
    name = "Demos",
    ext_modules = cythonize(ext_modules)
)

外部聲明(External declarations)

  • 如果你想調用一個 Cython 中沒有定義的函式宣告,那麼你必須自己進行聲明。例如,上文中的 sin()函數就是這樣定義的:
 cdef extern from "math.h":
    double sin(double x)
  • 此處聲明瞭sin()函數,這時我們便可在 Cython 代碼中使用這個函數,並且讓 Cython 生成一份包括math.h 標頭檔的 C 代碼。C 編譯器在編譯時能夠在math.h 中找到原始的函式宣告。但是 Cython 不能解析math.h 並需要一個單獨的定義。
  • 正如math 庫中的sin()函數一樣,只要 Cython 生成的模組正確的連結了共用庫或靜態程式庫,我們就可以聲明並調用任意的 C 庫函數。
  • 注意,只要簡單地通過cpdef 聲明,你就能從 Cython 模組中匯出一個外部 C 函數。而且生成了一個 Python 擴展,使得我們可以在 Python 代碼中直接訪問到 C 函數sin()。

變數的命名(Naming parameters)

  • C 和 Cython 都支持沒有參數明的signature declarations如下:
cdef extern from "string.h":
    char* strstr(const char*, const char*)
  • 然而,這樣的話 Cython 代碼將不能通過關鍵字參數來調用這個函數(由Cython 0.19及以後的版本所支持)。所以,我們最好這樣去聲明一個函數:
cdef extern from "string.h":
      char* strstr(const char *haystack, const char *needle)
  • 這會讓清楚地知道你所調用了哪兩個參數,從而能夠避免二義性並增強你的代碼的可讀性:
cdef char* data = "hfvcakdfagbcffvschvxcdfgccbcfhvgcsnfxjh"

pos = strstr(needle='akd', haystack=data)
print (pos != NULL)
  • 注意,正如 Python 代碼一樣,對已有參數名的修改是不向後相容的。那麼, 如果你為外部的 C 或 C++ 函數進行了自己的聲明,通常花一點時間去 將參數名命名的更好是非常值得的。

Memoryview切片

  • Cython 0.16中,增加了記憶體視圖(memoryview),用它可以很方便地存取NumPy陣列等支援buffer介面的物件中的資料。
  • 編寫如下三個檔,並保存到同一個目錄之下:
    • memview_test.py:用來測試編譯之後的擴展庫的測試程式
    • memview.pyx:Cython來源程式
    • setup.py:用於編譯Cython來源程式
  • setup.py
import numpy as np
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

setup(
    cmdclass = {'build_ext': build_ext},
    ext_modules = [
        Extension("memview", ["memview.pyx"],
    ],

    # 使用numpy套件必須加上此行
    include_dirs=[np.get_include()]
)
  • Memoryview切片(Memoryview slices)是Cython中的一種特殊類型,通過它可以高效地訪問支援buffer介面的Python物件內部的資料區,例如NumPy中的ndarray物件。下面我們通過一個例子說明它的用法
def memview_sum(int[:] a):
    # 參數a是一個一維整數切片類型,可以將與此切片類型一致的陣列傳遞給它
    cdef int i
    cdef int s = 0
    # 和NumPy陣列一樣,它的shape屬性為其各個軸的長度
    for i in range(a.shape[0]):
        s += a[i]
    return s
  • 分析編譯後的檔案 cython.py -a memview.pyx ,會生成memview.html與memview.c,可知 s+=a[i] 這一行這段代碼可以處理下標越界以及下標為負數的情況。由於需要在迴圈中對每個元素進行判斷,因此這些代碼會降低運算速度。
    /* "memview.pyx":5   
   *     cdef int s = 0
   *     for i in range(a.shape[0]):   
   *         s += a[i]             # <<<<<<<<<<<<<<
   *     return s   */
      __pyx_t_3 = __pyx_v_i;      
      __pyx_t_4 = -1;
      // 處理下標為負值及越界的狀況,每次迴圈都要處理,相當花時間
      if (__pyx_t_3 < 0) {        
        __pyx_t_3 += __pyx_v_a.shape[0];
        if (unlikely(__pyx_t_3 < 0)) __pyx_t_4 = 0;      
       } else if (unlikely(__pyx_t_3 >= __pyx_v_a.shape[0])) __pyx_t_4 = 0;
      if (unlikely(__pyx_t_4 != -1)) {
        __Pyx_RaiseBufferIndexError(__pyx_t_4);
        __PYX_ERR(0, 5, __pyx_L1_error)
      }
      __pyx_v_s = (__pyx_v_s + (*((int *) ( /* dim=0 */ (__pyx_v_a.data + __pyx_t_3 * __pyx_v_a.strides[0]) ))));
    }
  • 由於需要在迴圈中對每個元素進行判斷,因此這些代碼會降低運算速度。可以使用Cython提供的wraparound和boundscheck修飾器關閉這兩項功能。
cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)
def memview_sum(int[:] a):
    # ...
  • 使用修飾器對整個函數體有效,如果只希望對某一段程式有效的話,可以使用with關鍵字:
for i in range(a.shape[0]):
with cython.boundscheck(False):
    with cython.wraparound(False):
            s += a[i]
  • 關閉這兩個選項之後,輸出的代碼如下,很明顯可看出沒有檢查邊界。
/* "memview.pyx":9
 *     cdef int s = 0
 *     for i in range(a.shape[0]):
 *         s += a[i]             # <<<<<<<<<<<<<<
 *     return s
 */
    __pyx_t_3 = __pyx_v_i;
    __pyx_v_s = (__pyx_v_s + (*((int *) ( /* dim=0 */ (__pyx_v_a.data + __pyx_t_3 * __pyx_v_a.strides[0]) ))));
  }
  • 在控制檯中輸入
>>> a = np.arange(11)
>>> memview.memview_sum(a)
55
# 由於在C語言代碼中使用data和strides屬性訪問陣列中的資料,因此即使對於元素不是連續存儲的陣列也能正常運算。
>>> memview.memview_sum(a[::2]) ❶
30
  • 如果希望資料在記憶體中是連續存儲的,那麼可以用int[::1] a定義:
def memview_sum2(int[::1] a):
    cdef int i
    cdef int s = 0
    for i in range(a.shape[0]):
        s += a[i]
    return s

memview.memview_sum2(a)
55
>>> memview.memview_sum2(a[::2])
...
ValueError: ndarray is not C-contiguous
  • 多維記憶體視圖切片類型可以用如下方式定義:
cdef int[:, :]   # 二維切片
cdef int[:, ::1] # C語言連續(C-contiguous)的二維切片
cdef int[::1, :] # Fortran語言連續(Fortran-contiguous)的二維切片

記憶體視圖物件

  • 當將Cython的記憶體視圖切片類型返回到Python中時,它就變成了一個記憶體視圖物件。
def memview_object(int[:, :] a):
    # 參數a是一個二維切片類型
    # 它除了支援整數下標之外,還可以通過切片下標生成新的切片物件。
    # 我們直接將新生成的切片物件返回.
    return a[::2, ::2]
  • 在控制檯中執行如下代碼:

    b = np.arange(24).reshape(6, 4)
    >>> mv = memview.memview_object(b)
    >>> mv
    # 可得知傳回的是memoryview物件
    <MemoryView of 'ndarray' at 0x32bdb70>
    # 物件的屬性
    >>> dir(mv)
    [..., 'T', 'base', 'copy', 'copy_fortran', 'is_c_contig', 'is_f_contig', 'itemsize',
    'nbytes', 'ndim', 'shape', 'size', 'strides', 'suboffsets']
    >>> mv.shape
    (3, 2)
    
  • MemoryView物件的這些屬性和NumPy陣列十分類似。我們可以通過numpy.asarray()將MemoryView轉換為NumPy陣列:

    • 新創建的陣列c和原來的陣列b共用記憶體,因此修改c[0,0]會同時修改b[0,0]。
>>> c = np.asanyarray(mv)
>>> c
array([[ 0,  2],
       [ 8, 10],
       [16, 18]])
>>> c[0,0] = 100
>>> b[0,0]
100

Cython陣列

  • 當在Cython中調用切片物件的copy()和copy_fortran()時,將創建一個Cython陣列,並在此陣列上創建一個切片物件。在Python中調用MemoryView物件的copy()方法也與此類似。
>>> mv.base
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15],
       [16, 17, 18, 19],
       [20, 21, 22, 23]])
>>> mv2 = mv.copy()
>>> mv2
<MemoryView of 'array' at 0x317a210>
# 使用copy()後,得到不同的物件
>>> mv2.base
<memview.array object at 0x0311BA98>
  • 我們也可以在Cython中直接創建Cython陣列:
cimport cython.view
def cython_array(int w, int h):
    # 創建一個形狀為(h, w)的整型Cython陣列,
    # 並用一個記憶體視圖切片m對它進行存取。
    cdef int [:,:] m = cython.view.array(shape=(h, w), 
                                 itemsize=sizeof(int), format="i")
    cdef int i, j
    for i in range(h):
        for j in range(w):
            m[i, j] = i+j 
    return m

import pylab as pl
m = memview.cython_array(400, 250)
pl.imshow(m)
pl.show()

imgMemoryview繪圖

  • 如果要向C傳遞一個陣列來處理,大部分情況下應該是numpy的array,推薦使用Memoryview來接受python傳入的numpy的array。
    • Note: 如果numpy轉成memoryview時,將無法使用numpy的內建函數功能。
cdef int[:,:,:] view = np.arange(27, dtype=np.dtype("i")).reshape((3, 3, 3))
cdef int x[3][3][3]
cdef int[:,:,:] view = x
cdef int[:, :, ::1] c_contiguous = c_contig # store by row
cdef int[::1, :, :] f_contiguous = f_contig # Fortran store by column

cpdef histogram(int[:,:] image):
    import numpy as np
    cdef int[:] hist = np.zeros((256,),dtype=np.intc)
    for x in range(image.shape[0]):
        for y in range(image.shape[1]):
            hist[image[x,y]] += 1
    return np.asarray(hist)
  • 舊式numpy傳參數做法如下:
def func(np.ndarray[unsigned char, ndim=2, mode="c"] array not None):
    ...

C/C++和物件導向

  • 選擇:ctypes/CFFI/boost.python/ SWIG/cython
  • cython優點:
    • 易學,Python+C=Cython,重用舊知識 –
    • 好用,pxd,重用聲明檔
    • C++支持全面,可從C++中回檔Python函數,為Python class重載C++ class行為提供可能
  • 在Cython中也可以方便的使用物件導向的方式工作,只要使用cdef class就能在Cython中像在pure Python中那樣使用類:
  • 注意Cython中的類可以被pure Python中的類繼承,但反過來不行.
cdef class Rect:
    cdef int width, height

    def __init__(self, int w, int h):
        self.width = w
        self.height = h

    def area(self):
        return self.width*self.height

    def test_it(int x, int y):
        cdef Rect R = Rect(x,y)
        return R.area()
class shop:
    cdef object goods

    def __cinit__(self):
        self.goods = []

    property goods:
        def __get__(self):
            return "We have: %s" % self.goods
        def __set__(self, value):
            self.goods.append(value)
        def __del__(self):
            del self.goods[:]
  • 上面還涉及到cinit這個方法和原生python的init有些區別,前者可以更快的執行,官方的例子是:
  • 所以最求效率的化,儘量使用cinit吧。對於經常創建/刪除實例的類,可以在前面加上@cython.freelist(n)的裝飾器。可以獲得更好的性能。
cdef class Penguin:
    cdef object food

    def __cinit__(self, food):
        # 新式建構函式
        self.food = food

    def __init__(self, food):
        # python建構函式
        print("eating!")

normal_penguin = Penguin('fish')
fast_penguin = Penguin.__new__(Penguin, 'wheat')  # note: not calling __init__() !
  • 使用C++中的STL:
 from libcpp.vector cimport vector

 cdef vector[int] vect
 cdef int i
 for i in range(10):
     vect.push_back(i)
 for i in range(10):
     print vect[i]

 vect = xrange(1,10)
  • python到C++容器的轉換規則是 | python types | => c++ types | => python types| |---|---|---| | bytes | std:string | types| | iterable | std:vector | list| | iterable | std:list | list | | iterable | std:set | set | | iterable(len 2) | std:pair | tuple(len 2) |

直接使用C/C++代碼

  • 如果你恰好已經有了C部分的代碼,想直接在python中調用而不是用cython自己重寫的話,你只需要寫一個.pyx進行簡單的封裝,就能達到目的。

封裝

  • 如果只是一些C的函數需要封裝進來,使用cdef extern可以把C代碼中的函式宣告到cython中,當然你得有一個.c的檔來實現add函數。
cdef extern int add(int x, int y)

def add_py(int x, int y):
    return add(x, y)
  • 若是有一些C++的類需要封裝進來, 舉個官方的例子,你有一個rectangle.h
//rectangle.h

namespace shapes {
    class Rectangle {
    public:
        int x0, y0, x1, y1;
        Rectangle(int x0, int y0, int x1, int y1);
            int getArea();
        };
}

//rectangle.cpp
#include "Rectangle.h"
namespace shapes {
    Rectangle::Rectangle(int X0, int Y0, int X1, int Y1) {
        x0 = X0;
        y0 = Y0;
        x1 = X1;
        y1 = Y1;
    }

    int Rectangle::getArea() {
        return (x1 - x0) * (y1 - y0);
    }
}
# _rectangle.pyx

cdef extern from "Rectangle.h" namespace "shapes":
    cdef cppclass Rectangle:
        Rectangle(int, int, int, int) except +
            int x0, y0, x1, y1
            int getArea()

        def func():
            cdef Rectangle *rec = new Rectangle(1, 2, 3, 4)
            try:
                area = rec.getArea()
                return area
                ...
            finally:
                del rec     # delete heap allocated object

Python WebSocket長連接心跳與短連接

安裝

pip install websocket-client

先來看一下,長連接調用方式:

ws = websocket.WebSocketApp(
    "ws://echo.websocket.org/",
    on_message=on_message,
    on_error=on_error,
    on_close=on_close,
)
ws.on_open = on_open
ws.run_forever()

長連接,參數介紹:

(1)url: websocket的地址。

(2)header: 客戶發送websocket握手請求的請求頭,{'head1:value1','head2:value2'}。

(3)on_open:在建立Websocket握手時調用的可調用對象,這個方法只有一個參數,就是該類本身。

(4)on_message:這個對像在接收到服務器返回的消息時調用。有兩個參數,一個是該類本身,一個是我們從服務器獲取的字符串(utf-8格式)。

(5)on_error:這個對像在遇到錯誤時調用,有兩個參數,第一個是該類本身,第二個是異常對象。

(6)on_close:在遇到連接關閉的情況時調用,參數只有一個,就是該類本身。

(7)on_cont_message:這個對像在接收到連續幀數據時被調用,有三個參數,分別是:類本身,從服務器接受的字符串(utf-8),連續標志。

(8)on_data:當從服務器接收到消息時被調用,有四個參數,分別是:該類本身,接收到的字符串(utf-8),數據類型,連續標志。

(9)keep_running:一個二進制的標志位,如果為True,這個app的主循環將持續運行,默認值為True。

(10)get_mask_key:用於產生一個掩碼。

(11)subprotocols:一組可用的子協議,默認為空。

**長連接關鍵方法:**ws.run_forever(ping_interval=60,ping_timeout=5)

如果不斷開關閉websocket連接,會一直阻塞下去。另外這個函數帶兩個參數,如果傳的話,啟動心跳包發送。

ping_interval:自動發送“ping”命令,每個指定的時間(秒),如果設置為0,則不會自動發送。

ping_timeout:如果沒有收到pong消息,則為超時(秒)。

ws.run_forever(ping_interval=60, ping_timeout=5)
# ping_interval心跳發送間隔時間
# ping_timeout 設置,發送ping到收到pong的超時時間

我們看源代碼,會發現這樣一斷代碼:

ping的超時時間,要大於ping間隔時間

if not ping_timeout or ping_timeout <= 0:
    ping_timeout = None
if ping_timeout and ping_interval and ping_interval <= ping_timeout:
    raise WebSocketException("Ensure ping_interval > ping_timeout")

長連接:

示例1:

import websocket

try:
    import thread
except ImportError:
    import _thread as thread
import time


def on_message(ws, message):
    print(message)


def on_error(ws, error):
    print(error)


def on_close(ws):
    print("### closed ###")


def on_open(ws):
    def run(*args):
        ws.send("hello1")
        time.sleep(1)
        ws.close()

    thread.start_new_thread(run, ())


if __name__ == "__main__":
    websocket.enableTrace(True)
    ws = websocket.WebSocketApp(
        "ws://echo.websocket.org/",
        on_message=on_message,
        on_error=on_error,
        on_close=on_close,
    )
    ws.on_open = on_open
    ws.run_forever(ping_interval=60, ping_timeout=5)

示例2:

import websocket
from threading import Thread
import time
import sys


class MyApp(websocket.WebSocketApp):
    def on_message(self, message):
        print(message)

    def on_error(self, error):
        print(error)

    def on_close(self):
        print("### closed ###")

    def on_open(self):
        def run(*args):
            for i in range(3):
                # send the message, then wait
                # so thread doesn't exit and socket
                # isn't closed
                self.send("Hello %d" % i)
                time.sleep(1)

            time.sleep(1)
            self.close()
            print("Thread terminating...")

        Thread(target=run).start()


if __name__ == "__main__":
    websocket.enableTrace(True)
    if len(sys.argv) < 2:
        host = "ws://echo.websocket.org/"
    else:
        host = sys.argv[1]
    ws = MyApp(host)
    ws.run_forever()

短連接:

from websocket import create_connection
ws = create_connection("ws://echo.websocket.org/")
print("Sending 'Hello, World'...")
ws.send("Hello, World")
print("Sent")
print("Receiving...")
result =  ws.recv()
print("Received '%s'" % result)
ws.close()

出處

https://www.bbsmax.com/A/pRdBKapazn/


python websocket 斷線自動重連

先定義連接函數

def connection_tmp(ws):
    websocket.enableTrace(True)
    ws = websocket.WebSocketApp("ws://localhost:8000/ws",
                              on_message = on_message,
                            #   on_data=on_data_test,
                              on_error = on_error,
                              on_close = on_close)
    
    ws.on_open = on_open
    try:
        ws.run_forever()
    except KeyboardInterrupt:
        ws.close()  
    except:
        ws.close() 

再定義錯誤函數

def on_error(ws, error):

    global reconnect_count
    print(type(error))
    print(error)
    if type(error)==ConnectionRefusedError or type(error)==websocket._exceptions.WebSocketConnectionClosedException:
        print("正在嘗試第%d次重連"%reconnect_count)
        reconnect_count+=1
        if reconnect_count<100:
            connection_tmp(ws)
    else:
        print("其他error!")

設置屬性全部global即可

global reconnect_count
global ws
ws=None
reconnect_count=0
<class 'websocket._exceptions.WebSocketConnectionClosedException'>
Connection is already closed.
正在嘗試第4次重連
<class 'KeyboardInterrupt'>

其他error!
### closed ###!
### closed ###!
### closed ###!
### closed ###!
### closed ###!
### closed ###!

出處

https://www.cxybb.com/article/u013673826/105605631

bloomrpc

# Copyright 2015 gRPC authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The Python implementation of the GRPC helloworld.Greeter server."""

from concurrent import futures
import logging

import grpc
import helloworld_pb2
import helloworld_pb2_grpc


class Greeter(helloworld_pb2_grpc.GreeterServicer):

    def SayHello(self, request, context):
        return helloworld_pb2.HelloReply(message='Hello, %s!' % request.name)


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()


if __name__ == '__main__':
    logging.basicConfig()
    serve()
// Copyright 2021 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

syntax = "proto3";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. helloworld.proto 

客戶端就用 bloomrpc,不另外寫程式碼了。匯入對應的 proto 檔案,向 localhost:50051 傳送請求驗證服務有沒有正常工作:

concurrent.futures 平行任務處理

出處: https://steam.oxxostudio.tw/category/python/library/concurrent-futures.html

Python 在執行時,通常是採用同步的任務處理模式 ( 一個處理完成後才會接下去處理第二個 ),然而 Python 的標準函式「concurrent.futures」,提供了平行任務處理 ( 非同步 ) 的功能,能夠同時處理多個任務,這篇教學會介紹 concurrent.futures 的用法。

本篇使用的 Python 版本為 3.7.12,所有範例可使用 Google Colab 實作,不用安裝任何軟體 ( 參考:使用 Google Colab )

同步與非同步

同步和非同步的常見說法是:「同步模式下,每個任務必須按照順序執行,後面的任務必須等待前面的任務執行完成,在非同步模式下,後面的任務不用等前面的,各自執行各自的任務」,也可以想像成「同一個步道 vs 不同步道」,透過步道的方式,會更容易明白同步和非同步。( 因為有時會將同步與非同步的中文字面意思,想成「一起走」或「不要一起走」,很容易搞錯 )

  • 同步:「同一個步道」,只能依序排隊前進
  • 非同步:「不 ( 非 ) 同步道」,可以各走各的

Thread 和 Process

concurrent.futures 提供了 ThreadPoolExecutorProcessPoolExecutor 兩種可以平行處理任務的實作方法,ThreadPoolExecutor 是針對 Thread ( 執行緒 ),ProcessPoolExecutor 是針對 Process ( 程序 ),下方是 Thread 和 Process 的簡單差異說明:

英文中文說明
Thread執行緒程式執行任務的基本單位。
Process程序啟動應用程式時產生的執行實體,需要一定的 CPU 與記憶體資源,Process 由一到多個 Thread 組成,同一個 Process 裡的 Thread 可以共用記憶體資源。

import concurrent.futures

要使用 concurrent.futures 必須先 import concurrent.futures 模組,或使用 from 的方式,單獨 import 特定的類型。

更多資訊可以參考 Python 官方文件:concurrent.futures 啟動平行任務

import concurrent.futures
from concurrent.futures import ThreadPoolExecutor

ThreadPoolExecutor

ThreadPoolExecutor 會透過 Thread 的方式建立多個 Executors ( 執行器 ) ,執行並處理多個任務 ( tasks ),ThreadPoolExecutor 有四個參數,最常用的為 max_workers:

參數說明
max_workersThread 的數量,預設 5 ( CPU number * 5,每個 CPU 可以處理 5 個 Thread),數量越多,運行速度會越快,如果設定小於等於 0 會發生錯誤。
thread_name_prefixThread 的名稱,預設 ''。
initializer每個 Thread 啟動時調用的可調用對象,預設 None。
initargs傳遞給初始化程序的參數,使用 tuple,預設 ()。

使用 ThreadPoolExecutor 後,就能使用 Executors 的相關方法:

方法參數說明
submitfn, *args, **kwargs執行某個函式。
mapfunc, *iterables使用 map 的方式,使用某個函式執行可迭代的內容。
shutdownwait完成執行後回傳信號,釋放正在使用的任何資源,wait 預設 True 會在所有對象完成後才回傳信號,wait 設定 False 則會在執行後立刻回傳。

舉例來說,下方的程式碼執行後,會按照順序 ( 同步 ) 顯示出數字,前一個任務尚未處理完,就不會執行後續的工作。

import time
def test(n):
    for i in range(n):
        print(i, end=' ')
        time.sleep(0.2)

test(2)
test(3)
test(4)

# 0 1 0 1 2 0 1 2 3

如果改成 ThreadPoolExecutor 的方式,就會發現三個函式就會一起進行 ( 如果執行的函式大於 5,可再設定 max_workers 的數值 )。

import time
from concurrent.futures import ThreadPoolExecutor

def test(n):
    for i in range(n):
        print(i, end=' ')
        time.sleep(0.2)

executor = ThreadPoolExecutor()  # 設定一個執行 Thread 的啟動器

a = executor.submit(test, 2)     # 啟動第一個 test 函式
b = executor.submit(test, 3)     # 啟動第二個 test 函式
c = executor.submit(test, 4)     # 啟動第三個 test 函式
executor.shutdown()              # 關閉啟動器 ( 如果沒有使用,則啟動器會處在鎖住的狀態而無法繼續 )

# 0 0 0 1 1 1 2 2 3

上述的做法,可以改用 with...as 的方式 ( 有點類似 open 的 with )。

import time
from concurrent.futures import ThreadPoolExecutor

def test(n):
    for i in range(n):
        print(i, end=' ')
        time.sleep(0.2)

with ThreadPoolExecutor() as executor:    # 改用 with...as
    executor.submit(test, 2)
    executor.submit(test ,3)
    executor.submit(test, 4)

# 0 0 0 1 1 1 2 2 3

上述的範例,也可以改用 map 的做法:

import time
from concurrent.futures import ThreadPoolExecutor

def test(n):
    for i in range(n):
        print(i, end=' ')
        time.sleep(0.2)

with ThreadPoolExecutor() as executor:
    executor.map(test, [2,3,4])

# 0 0 0 1 1 1 2 2 3

輸入文字,停止函式執行

透過平行任務處理的方法,就能輕鬆做到「輸入文字,停止正在執行的函式」,以下方的例子而言,run 是一個具有「無窮迴圈」的函式,如果不使用平行任務處理,在 run 後方的程式都無法運作 ( 會被無窮迴圈卡住 ),而 keyin 是一個具有「input」指令的函式,如果不使用平行任務處理,在 keyin 後方的程式也無法運作 ( 會被 input 卡住 ),因此如果使用 concurrent.futures,就能讓兩個函式同時運行,搭配全域變數的做法,就能在輸入特定指令時,停止另外函式的運作。

import time
from concurrent.futures import ThreadPoolExecutor

a = True               # 定義 a 為 True

def run():
    global a           # 定義 a 是全域變數
    while a:           # 如果 a 為 True
        print(123)     # 不斷顯示 123
        time.sleep(1)  # 每隔一秒

def keyin():
    global a           # 定義 a 是全域變數
    if input() == 'a':
        a = False      # 如果輸入的是 a,就讓 a 為 False,停止 run 函式中的迴圈

executor = ThreadPoolExecutor()
e1 = executor.submit(run)
e2 = executor.submit(keyin)
executor.shutdown()

ProcessPoolExecutor

ProcessPoolExecutor 會透過 Process 的方式建立多個 Executors ( 執行器 ),執行並處理多個程序,ProcessPoolExecutor 有四個參數,最常用的為 max_workers:

參數說明
max_workersProcess 的數量,預設為機器的 CPU 數量,如果 max_workers 小於等於 0 或大於等於 61 會發生錯誤。
thread_name_prefixThread 的名稱,預設 ''。
initializer每個 Thread 啟動時調用的可調用對象,預設 None。
initargs傳遞給初始化程序的參數,使用 tuple,預設 ()。

使用 ProcessPoolExecutor 後,就能使用 Executors 的相關方法:

方法參數說明
submitfn, *args, **kwargs執行某個函式。
mapfunc, *iterables使用 map 的方式,使用某個函式執行可迭代的內容。
shutdownwait完成執行後回傳信號,釋放正在使用的任何資源,wait 預設 True 會在所有對象完成後才回傳信號,wait 設定 False 則會在執行後立刻回傳。

ProcessPoolExecutor 的用法基本上和 ThreadPoolExecutor 很像,但 ProcessPoolExecutor 主要會用做處理比較需要運算的程式,ThreadPoolExecutor 會使用於等待輸入和輸出 ( I/O ) 的程式,兩者執行後也會有些差別,ProcessPoolExecutor 執行後最後是顯示運算結果,而 ThreadPoolExecutor 則是顯示過程。

import time
from concurrent.futures import ProcessPoolExecutor

def test(n):
    for i in range(n):
        print(i, end=' ')
        time.sleep(0.2)
    print()

with ProcessPoolExecutor() as executor:
    executor.map(test, [4,5,6])

Python 教學 - concurrent.futures 平行任務處理

如果是使用 ThreadPoolExecutor 則會如下圖的結果:

Python 教學 - concurrent.futures 平行任務處理

此外,Python 3.5 之後 map() 方法多了 chunksize 參數可以使用,該參數只對 ProcessPoolExecutor 有效,可以提升處理大量可迭代物件的執行效能,chunksize 預設 1,數值越大效能越好 ( 以電腦本身 CPU 的效能為主 )。

import time
from concurrent.futures import ProcessPoolExecutor

def test(n):
    for i in range(n):
        print(i, end=' ')
        time.sleep(0.2)
    print()

with ProcessPoolExecutor() as executor:
    executor.map(test, [4,5,6], chunksize=5)  # 設定 chunksize

小結

Python 的 concurrent.futures 內建函式庫是一個相當方便的函式庫,不僅可以讓原本同步的執行變成非同步,大幅減少工作時間,用法上也比使用 multiprocessing、threading、asyncio 容易得多,是相當推薦的內建函式庫。

from multiprocessing import Process
from datetime import datetime
import time
import os

from schedule import Scheduler

class MPScheduler(Scheduler):
    def __init__(self, args=None, kwargs=None):
        if args is None:
            args = ()
        if kwargs is None:
            kwargs = {}
        super(MPScheduler, self).__init__(*args, **kwargs)
        # Among other things, this object inherits self.jobs (a list of jobs)
        self.args = args
        self.kwargs = kwargs
        self.processes = list()

    def _mp_run_job(self, job_func):
        """Spawn another process to run the job; multiprocessing avoids GIL issues"""
        job_process = Process(target=job_func, args=self.args,
            kwargs=self.kwargs)
        job_process.daemon = True
        job_process.start()
        self.processes.append(job_process)

    def run_pending(self):
        """Run any jobs which are ready"""
        runnable_jobs = (job_obj for job_obj in self.jobs if job_obj.should_run)
        for job_obj in sorted(runnable_jobs):
            job_obj.last_run = datetime.now()   # Housekeeping
            self._mp_run_job(job_obj.job_func)
            job_obj._schedule_next_run()        # Schedule the next execution datetime

        self._retire_finished_processes()

    def _retire_finished_processes(self):
        """Walk the list of processes and retire finished processes"""
        retirement_list = list()   # List of process objects to remove
        for idx, process in enumerate(self.processes):
            if process.is_alive():
                # wait a short time for process to finish
                process.join(0.01)
            else:
                retirement_list.append(idx)

        ## Retire finished processes
        for process_idx in sorted(retirement_list, reverse=True):
            self.processes.pop(process_idx)

def job(id, hungry=True):
    print("{} running {} and hungry={}".format(datetime.now(), id, hungry))
    time.sleep(10)   # This job runs without blocking execution of other jobs

if __name__=='__main__':
    # Build a schedule of overlapping jobs...
    mp_sched = MPScheduler()
    mp_sched.every(1).seconds.do(job, id=1, hungry=False)
    mp_sched.every(2).seconds.do(job, id=2)
    mp_sched.every(3).seconds.do(job, id=3)
    mp_sched.every(4).seconds.do(job, id=4)
    mp_sched.every(5).seconds.do(job, id=5)

    while True:
        mp_sched.run_pending()
        time.sleep(1)

新增&刪除任務排程

import time
import schedule
import redis


class TaskManager:
    def __init__(self):
        self.tasks = {}

    def add_task(
        self, condition_func, job_func, task_name, interval_seconds=15, tag=None
    ):
        self.tasks[task_name] = {
            "condition": condition_func,
            "job": job_func,
            "job_id": None,
            "interval": interval_seconds,
            "tag": tag,
        }

    def remove_task(self, task_name):
        if task_name in self.tasks:
            if self.tasks[task_name]["job_id"] is not None:
                schedule.clear(self.tasks[task_name]["job_id"])
            del self.tasks[task_name]

    def run(self):
        while True:
            schedule.run_pending()
            for task_name, task in self.tasks.items():
                if task["condition"]():
                    if task["job_id"] is None:
                        print("Task added, task name: ", task_name)
                        task["job"]()
                        task["job_id"] = (
                            schedule.every(task["interval"])
                            .seconds.do(task["job"])
                            .tag(task["tag"])
                        )
                else:
                    if task["job_id"] is not None:
                        print(f"Task name: {task_name}", task["job_id"])
                        schedule.clear(tag=task["tag"])
                    task["job_id"] = None
            time.sleep(1)


if __name__ == "__main__":
    task_manager = TaskManager()
    redis_client = redis.StrictRedis(
        host="localhost", port=6379, db=0
    )  # 修改為您的Redis服務器配置

    def task1_condition():
        condition_value = redis_client.get("task_test")
        return condition_value and condition_value.decode() == "True"

    def task1_job():
        print("Task 1 - Hello")

    task_manager.add_task(task1_condition, task1_job, "task_test", 3, tag="mm_tasks")
    task_manager.run()

# 開關 task_test 測試
import redis

# 創建Redis連接
redis_client = redis.StrictRedis(host="localhost", port=6379, db=0)  # 修改為您的Redis服務器配置

# 將鍵設置為True
# redis_client.set("task_test", "True")
redis_client.set("task_test", "False")

Parallel execution

https://schedule.readthedocs.io/en/stable/parallel-execution.html

am trying to execute 50 items every 10 seconds, but from the my logs it says it executes every item in 10 second schedule serially, is there a work around?

By default, schedule executes all jobs serially. The reasoning behind this is that it would be difficult to find a model for parallel execution that makes everyone happy.

You can work around this limitation by running each of the jobs in its own thread:

import threading
import time
import schedule

def job():
    print("I'm running on thread %s" % threading.current_thread())

def run_threaded(job_func):
    job_thread = threading.Thread(target=job_func)
    job_thread.start()

schedule.every(10).seconds.do(run_threaded, job)
schedule.every(10).seconds.do(run_threaded, job)
schedule.every(10).seconds.do(run_threaded, job)
schedule.every(10).seconds.do(run_threaded, job)
schedule.every(10).seconds.do(run_threaded, job)


while 1:
    schedule.run_pending()
    time.sleep(1)

If you want tighter control on the number of threads use a shared jobqueue and one or more worker threads:

import time
import threading
import schedule
import queue

def job():
    print("I'm working")


def worker_main():
    while 1:
        job_func = jobqueue.get()
        job_func()
        jobqueue.task_done()

jobqueue = queue.Queue()

schedule.every(10).seconds.do(jobqueue.put, job)
schedule.every(10).seconds.do(jobqueue.put, job)
schedule.every(10).seconds.do(jobqueue.put, job)
schedule.every(10).seconds.do(jobqueue.put, job)
schedule.every(10).seconds.do(jobqueue.put, job)

worker_thread = threading.Thread(target=worker_main)
worker_thread.start()

while 1:
    schedule.run_pending()
    time.sleep(1)

This model also makes sense for a distributed application where the workers are separate processes that receive jobs from a distributed work queue. I like using beanstalkd with the beanstalkc Python library.

python 任務調度之 schedule

http://puremonkey2010.blogspot.com/2019/05/python-python-schedule.html

在工作中多少都會涉及到一些定時任務,比如定時郵件提醒等.本文通過開源項目 schedule 來學習定時任務調度是如何工作的,以及基於此實現一個 web 版本的提醒工具.

import threading
import schedule
import time


def job():
    print(f"I'm working... ThreadID:{threading.get_ident()}")


schedule.every(1).seconds.do(job)
schedule.every(10).minutes.do(job)
schedule.every().hour.do(job)
schedule.every().day.at("10:30").do(job)
schedule.every(5).to(10).minutes.do(job)
schedule.every().monday.do(job)
schedule.every().wednesday.at("13:15").do(job)
# schedule.every().day.at("12:42", "Europe/Amsterdam").do(job)
schedule.every().minute.at(":17").do(job)


def job_with_argument(name):
    print(f"I am {name} ThreadID:{threading.get_ident()}")


schedule.every(5).seconds.do(job_with_argument, name="Peter")

while True:
    schedule.run_pending()
    time.sleep(1)

import schedule  
import time  
  
def job():  
    print("I'm working...")  
  
schedule.every(10).minutes.do(job)  
schedule.every().hour.do(job)  
schedule.every().day.at("10:30").do(job)  
schedule.every().monday.do(job)  
schedule.every().wednesday.at("13:15").do(job)  
  
while True:  
    schedule.run_pending()  
    time.sleep(1)  
每隔10分鐘執行一次任務
每隔一小時執行一次任務
每天10:30執行一次任務
每週一的這個時候執行一次任務
每週三13:15執行一次任務
class Scheduler(object):  
    """  
    Objects instantiated by the :class:`Scheduler ` are  
    factories to create jobs, keep record of scheduled jobs and  
    handle their execution.  
    """  
    def __init__(self):  
        self.jobs = []  
  
    def run_pending(self):  
        runnable_jobs = (job for job in self.jobs if job.should_run)  
        for job in sorted(runnable_jobs):  
            self._run_job(job)  
  
    def run_all(self, delay_seconds=0):  
        logger.info('Running *all* %i jobs with %is delay inbetween',  
                    len(self.jobs), delay_seconds)  
        for job in self.jobs[:]:  
            self._run_job(job)  
            time.sleep(delay_seconds)  
  
    def clear(self, tag=None):  
        if tag is None:  
            del self.jobs[:]  
        else:  
            self.jobs[:] = (job for job in self.jobs if tag not in job.tags)  
  
    def cancel_job(self, job):  
        try:  
            self.jobs.remove(job)  
        except ValueError:  
            pass  
  
    def every(self, interval=1):  
        job = Job(interval, self)  
        return job  
  
    def _run_job(self, job):  
        ret = job.run()  
        if isinstance(ret, CancelJob) or ret is CancelJob:  
            self.cancel_job(job)  
  
    @property  
    def next_run(self):  
        if not self.jobs:  
            return None  
        return min(self.jobs).next_run  
  
    @property  
    def idle_seconds(self):  
        return (self.next_run - datetime.datetime.now()).total_seconds()  

Scheduler 作用就是在 job 可以執行的時候執行它. 這裡的函數也都比較簡單:

*** run_pending:** 運行所有可以運行的任務 *** run_all:** 運行所有任務,不管是否應該運行 *** clear:** 刪除所有調度的任務 *** cancel_job:** 刪除一個任務 *** every:** 創建一個調度任務, 返回的是一個 Job 物件 *** _run_job:** 運行一個 Job 物件 *** next_run:** 獲取下一個要運行任務的時間, 這裡使用的是 min 去得到最近將執行的 job, 之所以這樣使用,是 Job 重載了lt 方法,這樣寫起來確實很簡潔. *** idle_seconds:** 還有多少秒即將開始運行任務.

Class Job Job 是整個定時任務的核心. 主要功能就是根據創建 Job 時的參數, 得到下一次運行的時間. 代碼如下,稍微有點長 (會省略部分代碼,可以看 源碼). 這個類別提供的ˊ方法也不是很多, 有很多邏輯是一樣的. 簡單介紹一下建構子的參數:

*** interval:** 間隔多久,每 interval 秒或分等. *** job_func:** job 執行函數 *** unit :** 間隔單元,比如 minutes, hours *** at_time:** job 具體執行時間點,比如 10:30等 *** last_run:** job上一次執行時間 *** next_run:** job下一次即將運行時間 *** period:** 距離下次運行間隔時間 *** start_day:** 週的特殊天,也就是 monday 等的含義

再來看一下幾個重要的方法: *** lt:**

被使用在比較哪個 job 最先即將執行, Scheduler 中 next_run 方法裡使用 min 會用到, 有時合適的使用 python 這些特殊方法可以簡化代碼,看起來更 pythonic.

*** second、seconds:**

second、seconds 的區別就是 second 時默認 interval ==1, 即 schedule.every().second 和 schedule.every(1).seconds 是等價的,作用就是設置 unit 為 seconds. minute 和 minutes、hour 和hours 、day 和 days、week 和 weeks 也類似.

*** monday:**

設置 *start_day* 為 monday, unit 為 weeks, interval 為1 . 含義就是每週一執行 job. 類似 tuesday、wednesday、thursday、friday、saturday、sunday 一樣.

*** at:**

表示 某天的某個時間點,所以不適合 minutes、weeks 且 start_day 為空 (即單純的周) 這些 unit. 對於 unit 為 hours 時, time_str 中小時部分為 0.

*** do:**

設置 job 對應的函數以及參數, 這裡使用 functools.update_wrapper 去更新函數名等信息.主要是 functools.partial 返回的函數和原函數名稱不一樣.具體可以看看官網文檔. 然後調用 _schedule_next_run 去計算 job 下一次執行時間.

*** should_run:**

判斷 job 是否可以運行了.依據是當前時間點大於等於 job 的 next_run

*** _schedule_next_run:**

這是整個 job 的定時的邏輯部分是計算 job 下次運行的時間點的. 這邊描述一下流程, 首先是計算下一次執行時間:

view plaincopy to clipboardprint?

  1. self.period = datetime.timedelta(**{self.unit: interval})
  2. self.next_run = datetime.datetime.now() + self.period

這裡根據

unit

interval

計算出下一次運行時間. 舉個例子,比如

schedule.every().hour.do(job, message='things')

下一次運行時間就是當前時間加上一小時的間隔. 但是當

start_day

不為空時,即表示某個星期. 這時

period

就不能直接加在當前時間了. 看代碼:

view plaincopy to clipboardprint?

  1. weekday = weekdays.index(self.start_day)
  2. days_ahead = weekday - self.next_run.weekday()
  3. if days_ahead <= 0: # Target day already happened this week
  4. days_ahead += 7
  5. self.next_run += datetime.timedelta(days_ahead) - self.period

其中

days_ahead

表示 job 表示的星期幾與當表示的星期幾差幾天. 比如今天是 星期三,job 表示的是 星期五,那麼

days_ahead

就為2,最終

self.next_run

效果就是在 now 基礎上加了2天.

接著當

at_time

不為空時, 需要更新執行的時間點,具體就是計算時、分、秒然後調用 replace 進行更新.

Real User Cases 這邊介紹實際使用範例.

在 *N* 小時/分鐘 後執行並只一次 這個範例很像 Linux 命令 at 的功能, 簡單來說就是延遲一段時間後再執行某個 job. 這邊我們會繼承 Job 並客製成我們需要的功能 MyJob 類別: - test_run_after.py

#!/usr/bin/env python3  
import schedule  
import logging  
import functools  
import os  
import re  
import time  
from schedule import Job, CancelJob, IntervalError  
from datetime import datetime, timedelta  
  
logging.basicConfig(level=logging.INFO)  
logger = logging.getLogger(os.path.basename(__file__))  
logger.setLevel(20)  
  
class MyJob(Job):  
    def __init__(self, scheduler=None):  
        super(MyJob, self).__init__(1, scheduler)  
        self.regex = re.compile(r'((?P\d+?)hr)?((?P\d+?)m)?((?P\d+?)s)?')  
  
    def parse_time(self, time_str):  
        # https://stackoverflow.com/questions/4628122/how-to-construct-a-timedelta-object-from-a-simple-string  
        parts = self.regex.match(time_str)  
        if not parts:  
            raise IntervalError()  
  
        parts = parts.groupdict()  
        time_params = {}  
        for (name, param) in parts.items():  
            if param:  
                time_params[name] = int(param)  
  
        return timedelta(**time_params)  
  
    def do(self, job_func, *args, **kwargs):  
        self.job_func = functools.partial(job_func, *args, **kwargs)  
        try:  
            functools.update_wrapper(self.job_func, job_func)  
        except AttributeError:  
            # job_funcs already wrapped by functools.partial won't have  
            # __name__, __module__ or __doc__ and the update_wrapper()  
            # call will fail.  
            pass  
  
        self.scheduler.jobs.append(self)  
        return self  
  
    def after(self, atime):  
        if isinstance(atime, timedelta):  
            self.next_run = datetime.now() + atime  
        elif isinstance(atime, str):  
            times = atime.split(':')  
            if len(times) == 3:  # HH:MM:SS  
                self.next_run = datetime.now() + timedelta(hours=int(times[0]), minutes=int(times[1]), seconds=int(times[2]))  
            else:  
                self.next_run = datetime.now() + self.parse_time(atime)  
        else:  
            raise IntervalError()  
  
        return self  
  
    def run(self):  
        logger.info('Running job %s', self)  
        ret = self.job_func()  
        self.last_run = datetime.now()  
        return CancelJob()  
  
def main():  
    def work():  
        logger.info('Work done at {}'.format(datetime.now()))  
  
    myjob = MyJob(schedule.default_scheduler)  
    myjob.after('2m').do(work)  # Do work after 2 minutes  
  
    logger.info('Now is {}'.format(datetime.now()))  
    while len(schedule.default_scheduler.jobs) > 0:  
        schedule.run_pending()  
        time.sleep(1)  
  
    logger.info('All job done!')  
  
  
if __name__ == '__main__':  
    main()  

Execution result:

**#** ./test_run_after.py
INFO:test_run_after.py:Now is 2019-05-23 13:57:06.289055
INFO:test_run_after.py:Running job functools.partial(.work at 0x7f7d85a43950>)
INFO:test_run_after.py:Work done at 2019-05-23 13:59:06.438432
INFO:test_run_after.py:All job done!

https://zhuanlan.zhihu.com/p/537722631

安裝

pip install schedule

不適合 schedule 的情況

說實話,Schedule不是一個“一刀切”的調度庫。此庫旨在成為簡單調度問題的簡單解決方案。如果需要以下需求,您可能應該在其他地方尋找可用方案:

  • 作業持久性(記住重新啟動之間的計畫)
  • 精確計時(亞秒級精度執行)
  • 並行執行(多個執行緒)
  • 本地化(時區、工作日或節假日)

Schedule不考慮執行作業函數所需的時間。為了保證穩定的執行計畫,您需要將長時間運行的作業移出主執行緒(計畫程式執行的位置)。有關示例實現,請參閱平行執行

使用示例

普通方法

import schedule
import time

def job():
    print("I'm working...")

schedule.every(10).minutes.do(job) # 每十分鐘
schedule.every().hour.do(job) # 每小時
schedule.every().day.at("10:30").do(job) # 每天10:30
schedule.every().monday.do(job) # 每月
schedule.every().wednesday.at("13:15").do(job) # 每週三 13:15
schedule.every().minute.at(":17").do(job) # 每分鐘的第17秒

while True:
    schedule.run_pending()
    time.sleep(1)

裝飾器方法

from schedule import every, repeat, run_pending
import time

@repeat(every(10).minutes)
def job():
    print("I am a scheduled job")

while True:
    run_pending()
    time.sleep(1)

向任務傳參

import schedule

def greet(name):
    print('Hello', name)

schedule.every(2).seconds.do(greet, name='Alice')
schedule.every(4).seconds.do(greet, name='Bob')

from schedule import every, repeat

@repeat(every().second, "World")
@repeat(every().day, "Mars")
def hello(planet):
    print("Hello", planet)

取消任務

import schedule

def some_task():
    print('Hello world')

job = schedule.every().day.at('22:30').do(some_task)
schedule.cancel_job(job) # 取消任務

只運行某任務一次

import schedule
import time

def job_that_executes_once():
    # Do some work that only needs to happen once...
    return schedule.CancelJob # 通過返回schedule.CancelJob,將其在 scheduler 中取消

schedule.every().day.at('22:30').do(job_that_executes_once)

while True:
    schedule.run_pending()
    time.sleep(1)

獲取所有任務

要從調度程序中檢索所有作業,請使用 schedule.get_jobs()

import schedule

def hello():
    print('Hello world')

schedule.every().second.do(hello)

all_jobs = schedule.get_jobs()

取消所有任務

要從調度程序中刪除所有作業,請使用 schedule.clear()

import schedule

def greet(name):
    print('Hello {}'.format(name))

schedule.every().second.do(greet)

schedule.clear()

獲得多個工作,按標籤過濾

您可以從調度程序中檢索一組作業,並通過唯一識別碼選擇它們。

import schedule

def greet(name):
    print('Hello {}'.format(name))

schedule.every().day.do(greet, 'Andrea').tag('daily-tasks', 'friend')
schedule.every().hour.do(greet, 'John').tag('hourly-tasks', 'friend')
schedule.every().hour.do(greet, 'Monica').tag('hourly-tasks', 'customer')
schedule.every().day.do(greet, 'Derek').tag('daily-tasks', 'guest')

friends = schedule.get_jobs('friend')

取消多個作業,按標籤過濾

import schedule

def greet(name):
    print('Hello {}'.format(name))

schedule.every().day.do(greet, 'Andrea').tag('daily-tasks', 'friend')
schedule.every().hour.do(greet, 'John').tag('hourly-tasks', 'friend')
schedule.every().hour.do(greet, 'Monica').tag('hourly-tasks', 'customer')
schedule.every().day.do(greet, 'Derek').tag('daily-tasks', 'guest')

schedule.clear('daily-tasks')

以隨機間隔運行作業

def my_job():
    print('Foo')

# Run every 5 to 10 seconds.
schedule.every(5).to(10).seconds.do(my_job)

運行作業直到特定時間

import schedule
from datetime import datetime, timedelta, time

def job():
    print('Boo')

# run job until a 18:30 today
schedule.every(1).hours.until("18:30").do(job)

# run job until a 2030-01-01 18:33 today
schedule.every(1).hours.until("2030-01-01 18:33").do(job)

# Schedule a job to run for the next 8 hours
schedule.every(1).hours.until(timedelta(hours=8)).do(job)

# Run my_job until today 11:33:42
schedule.every(1).hours.until(time(11, 33, 42)).do(job)

# run job until a specific datetime
schedule.every(1).hours.until(datetime(2020, 5, 17, 11, 36, 20)).do(job)

until 方法設定作業的截止時間。 該作業將不會在截止時間之後運行。

距離下一次執行的時間

使用 schedule.idle_seconds() 獲取下一個作業計畫運行之前的秒數。 如果下一個計畫的作業計畫在過去運行,則返回值為負。 如果沒有安排作業,則返回 None

import schedule
import time

def job():
    print('Hello')

schedule.every(5).seconds.do(job)

while 1:
    n = schedule.idle_seconds()
    if n is None:
        # no more jobs
        break
    elif n > 0:
        # sleep exactly the right amount of time
        time.sleep(n)
    schedule.run_pending()

立即運行所有作業,無論它們的日程安排如何

要運行所有作業,無論它們是否計畫運行,請使用 schedule.run_all()。 完成後會重新安排作業,就像使用 run_pending() 執行作業一樣。

import schedule

def job_1():
    print('Foo')

def job_2():
    print('Bar')

schedule.every().monday.at("12:40").do(job_1)
schedule.every().tuesday.at("16:40").do(job_2)

schedule.run_all()

# Add the delay_seconds argument to run the jobs with a number
# of seconds delay in between.
schedule.run_all(delay_seconds=10)

在背景執行

不可能在背景執行 scheduleOut of the box it is not possible to run the schedule in the background. 但是,您可以自己建立一個執行緒並使用它來運行作業而不會阻塞主執行緒。 這是您如何執行此操作的示例:

import threading
import time

import schedule


def run_continuously(interval=1):
    """Continuously run, while executing pending jobs at each
    elapsed time interval.
    @return cease_continuous_run: threading. Event which can
    be set to cease continuous run. Please note that it is
    *intended behavior that run_continuously() does not run
    missed jobs*. For example, if you've registered a job that
    should run every minute and you set a continuous run
    interval of one hour then your job won't be run 60 times
    at each interval but only once.
    """
    cease_continuous_run = threading.Event()

    class ScheduleThread(threading.Thread):
        @classmethod
        def run(cls):
            while not cease_continuous_run.is_set():
                schedule.run_pending()
                time.sleep(interval)

    continuous_thread = ScheduleThread()
    continuous_thread.start()
    return cease_continuous_run


def background_job():
    print('Hello from the background thread')


schedule.every().second.do(background_job)

# Start the background thread
stop_run_continuously = run_continuously()

# Do some other things...
time.sleep(10)

# Stop the background thread
stop_run_continuously.set()

平行執行

我試圖每 10 秒執行 50 個項目,但是從我的日誌中它說它在 10 秒的計畫中連續執行每個項目,有解決方法嗎?

默認情況下,schedule順序執行所有作業。 這背後的原因是,很難找到一個讓每個人都滿意的平行執行模型。

您可以通過在其自己的執行緒中運行每個作業來解決此限制:

import threading
import time
import schedule

def job():
    print("I'm running on thread %s" % threading.current_thread())

def run_threaded(job_func):
    job_thread = threading.Thread(target=job_func)
    job_thread.start()

schedule.every(10).seconds.do(run_threaded, job)
schedule.every(10).seconds.do(run_threaded, job)
schedule.every(10).seconds.do(run_threaded, job)
schedule.every(10).seconds.do(run_threaded, job)
schedule.every(10).seconds.do(run_threaded, job)


while 1:
    schedule.run_pending()
    time.sleep(1)

如果您想更嚴格地控制執行緒數,請使用共享作業佇列和一個或多個工作執行緒:

import time
import threading
import schedule
import queue

def job():
    print("I'm working")


def worker_main():
    while 1:
        job_func = jobqueue.get()
        job_func()
        jobqueue.task_done()

jobqueue = queue.Queue()

schedule.every(10).seconds.do(jobqueue.put, job)
schedule.every(10).seconds.do(jobqueue.put, job)
schedule.every(10).seconds.do(jobqueue.put, job)
schedule.every(10).seconds.do(jobqueue.put, job)
schedule.every(10).seconds.do(jobqueue.put, job)

worker_thread = threading.Thread(target=worker_main)
worker_thread.start()

while 1:
    schedule.run_pending()
    time.sleep(1)

該模型對於分佈式應用程式也很有意義,其中工作人員是從分佈式工作佇列接收作業的獨立處理程序。 我喜歡將 beanstalkd 與 beanstalkc Python 庫一起使用。

異常處理

Schedule****不會捕獲作業執行期間發生的異常。 因此,在作業執行期間拋出的任何異常都會冒泡並中斷 schedule 的 run_xyz 函數

如果你想防止異常,你可以將你的工作函數包裝在一個裝飾器中,如下所示:

import functools

def catch_exceptions(cancel_on_failure=False):
    def catch_exceptions_decorator(job_func):
        @functools.wraps(job_func)
        def wrapper(*args, **kwargs):
            try:
                return job_func(*args, **kwargs)
            except:
                import traceback
                print(traceback.format_exc())
                if cancel_on_failure:
                    return schedule.CancelJob
        return wrapper
    return catch_exceptions_decorator

@catch_exceptions(cancel_on_failure=True)
def bad_task():
    return 1 / 0

schedule.every(5).minutes.do(bad_task)

日誌(Logging)

Schedule 將消息記錄到名為 schedule 在 DEBUG 等級的 Python 記錄器。 要從 Schedule 接收日誌,請將日誌(logging)記錄等級設定為 DEBUG。

import schedule
import logging

logging.basicConfig()
schedule_logger = logging.getLogger('schedule')
schedule_logger.setLevel(level=logging.DEBUG)

def job():
    print("Hello, Logs")

schedule.every().second.do(job)

schedule.run_all()

schedule.clear()

這將生成以下日誌消息:

DEBUG:schedule:Running *all* 1 jobs with 0s delay in between
DEBUG:schedule:Running job Job(interval=1, unit=seconds, do=job, args=(), kwargs={})
Hello, Logs
DEBUG:schedule:Deleting *all* jobs

自訂日誌記錄

向作業新增可重用日誌的最簡單方法是實現一個處理日誌的裝飾器。 例如,下面的程式碼新增了 print_elapsed_time 裝飾器:

import functools
import time
import schedule

# This decorator can be applied to any job function to log the elapsed time of each job
def print_elapsed_time(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_timestamp = time.time()
        print('LOG: Running job "%s"' % func.__name__)
        result = func(*args, **kwargs)
        print('LOG: Job "%s" completed in %d seconds' % (func.__name__, time.time() - start_timestamp))
        return result

    return wrapper


@print_elapsed_time
def job():
    print('Hello, Logs')
    time.sleep(5)

schedule.every().second.do(job)

schedule.run_all()

輸出:

LOG: Running job "job"
Hello, Logs
LOG: Job "job" completed in 5 seconds

多個調度器

您可以根據需要從單個調度程式執行任意數量的作業。 但是,對於較大的安裝,可能需要多個調度程序。 這是支援的

import time
import schedule

def fooJob():
    print("Foo")

def barJob():
    print("Bar")

# Create a new scheduler
scheduler1 = schedule.Scheduler()

# Add jobs to the created scheduler
scheduler1.every().hour.do(fooJob)
scheduler1.every().hour.do(barJob)

# Create as many schedulers as you need
scheduler2 = schedule.Scheduler()
scheduler2.every().second.do(fooJob)
scheduler2.every().second.do(barJob)

while True:
    # run_pending needs to be called on every scheduler
    scheduler1.run_pending()
    scheduler2.run_pending()
    time.sleep(1)

How To Draw Stock Chart With Python

出處: https://pythoninoffice.com/draw-stock-chart-with-python/

In this tutorial, we’ll learn how to draw a stock chart with Python. Static charts are so 1990s, we don’t do it here. Instead, we’ll draw fully interactive charts using plotly.

UPDATED April 8, 2022 – Include a correction to hide non-trading days.

Tools Required

plotly – AWESOME charting library

yfinance – download historical market data from Yahoo Finance

pip install plotly
pip install yfinance

Download Historical Price Data From Yahoo Finance

We are going to use the yfinance library to download Tesla stock historical (1 year) price data. yfinance makes it really simple to download stock price data from Yahoo Finance.

import yfinance
tsla = yfinance.Ticker('TSLA')
hist = tsla.history(period='1y')

img

Tesla Stock Historical Price Data

Start With A Simple Stock Chart Using Python

In a previous tutorial, we talked about how to use Plotly Express. However, due to the complexity of our stock chart, we’ll need to use the regular plotly to unlock its true power.

It’s kinda funny that we can use the .Scatter() to draw a line chart. The following code draws a stock price chart using the daily Close price, also note the mode='lines'. It’s also important to remember to .show() the chart after plotting otherwise we can’t see them.

By the way, these charts are interactive, you can hover the mouse over the chart and see the price details.

import plotly.graph_objects as go

fig = go.Figure(data=go.Scatter(x=hist.index,y=hist['Close'], mode='lines'))
fig.show()

img

Tesla Stock Historical Price Data

If we set the mode='markers', then we’ll have a regular scatter (dots) plot. There’s also another mode='lines+markers' that shows both dots and lines like the below.

fig = go.Figure(data=go.Scatter(x=hist.index,y=hist['Close'], mode='lines+markers'))
fig.show()

img

Add Trading Volume To The Stock Chart

Let’s add the trading volume to the chart. To do this, we’ll need a subplot and secondary_y axis for the volume data.

In general, we can use the figure.add_trace() method to add a new data series into the graph. This is something the Plotly Express has difficulty with but is very easy to achieve in plotly_objects.

Note that for the primary y-axis i.e. the first figure.add_trace() below, we need to include secondary_y=False, or leave it blank (so that it will default to False). For the secondary y-axis, we need to specify that secondary_y=True in the add_trace method.

from plotly.subplots import make_subplots

fig2 = make_subplots(specs=[[{"secondary_y": True}]])
fig2.add_trace(go.Scatter(x=hist.index,y=hist['Close'],name='Price'),secondary_y=False)
fig2.add_trace(go.Bar(x=hist.index,y=hist['Volume'],name='Volume'),secondary_y=True)
fig2.show()

img

Although the volume data is on the secondary y-axis (see the label on the right-hand side), some of the bars are way too long and are covering the stock price graph. Let’s scale the volume bars down a little bit by setting a range for the y-axis. We can use the figure.update_yaxes() method to do that. Also, I’m going to hide the number labeling for the volume data. Again, note that we need to specify that we are operating on the secondary y-axis by setting secondary_y=True in the below code.

fig2.update_yaxes(range=[0,7000000000],secondary_y=True)
fig2.update_yaxes(visible=False, secondary_y=True)

img

Candlestick Chart

So really, who looks at a line chart for stocks? Pros look at only the candlestick chart!

No problem, we can do it in the candlestick style. Instead of using the Scatter() plot and passing the ‘Close’ price to the y-axis, now we need to specify each of ‘open’, ‘high’, ‘low’ and ‘close’, also known as the “ohlc”.

fig3 = make_subplots(specs=[[{"secondary_y": True}]])
fig3.add_trace(go.Candlestick(x=hist.index,
                              open=hist['Open'],
                              high=hist['High'],
                              low=hist['Low'],
                              close=hist['Close'],
                             ))

img

It’s interesting because, with the Candlestick chart, we now have another smaller chart at the bottom, this is actually called a “range slider”, and we can drag either side to zoom in/out on a certain area of the chart.

Let’s also add back the volume information to the chart. I don’t think the range slider is particularly useful in this case, so I’m going to hide it by using the figure.update_layout() method.

fig3.add_trace(go.Bar(x=hist.index, y=hist['Volume'], name='Volume'),secondary_y=True)
fig3.update_layout(xaxis_rangeslider_visible=False)

Indicators

We’ll draw a simple indicator 20 Day Moving Average here to show the concept, theoretically, we can plot any indicator on the chart.

pandas provides convenient ways to calculate time series-related metrics such as the moving average. The df.rolling() method provides “moving windows” that we can operate on. To get the average of the moving window, we just need to add the .mean() at the end of the rolling() method.

fig3.add_trace(go.Scatter(x=hist.index,y=hist['Close'].rolling(window=20).mean(),marker_color='blue',name='20 Day MA'))
fig3.add_trace(go.Bar(x=hist.index, y=hist['Volume'], name='Volume'),secondary_y=True)
fig3.update_layout(title={'text':'TSLA', 'x':0.5})
fig3.update_yaxes(range=[0,1000000000],secondary_y=True)
fig3.update_yaxes(visible=False, secondary_y=True)
fig3.update_layout(xaxis_rangeslider_visible=False)  #hide range slider
fig3.show()

We are also going to modify the volume a little bit. Right now the volume bars all have the same color. We can use different colors to distinguish between an up or down day – green for up days, and red for down days.

To do that, we just need to calculate the daily change (positive or negative) then insert a color column into our dataframe. Then we can pass the color information into the volume data series. The marker argument dictates how our scatter plot should look like – color, shape, size, etc.

hist['diff'] = hist['Close'] - hist['Open']
hist.loc[hist['diff']>=0, 'color'] = 'green'
hist.loc[hist['diff']<0, 'color'] = 'red'

imgAdd a column to indicate color

To put everything together:

fig3 = make_subplots(specs=[[{"secondary_y": True}]])
fig3.add_trace(go.Candlestick(x=hist.index,
                              open=hist['Open'],
                              high=hist['High'],
                              low=hist['Low'],
                              close=hist['Close'],
                              name='Price'))
fig3.add_trace(go.Scatter(x=hist.index,y=hist['Close'].rolling(window=20).mean(),marker_color='blue',name='20 Day MA'))
fig3.add_trace(go.Bar(x=hist.index, y=hist['Volume'], name='Volume', marker={'color':hist['color']}),secondary_y=True)
fig3.update_yaxes(range=[0,700000000],secondary_y=True)
fig3.update_yaxes(visible=False, secondary_y=True)
fig3.update_layout(xaxis_rangeslider_visible=False)  #hide range slider
fig3.update_layout(title={'text':'TSLA', 'x':0.5})
fig3.show()

img

You might notice that in the above graph, 20 D MA didn’t start from the beginning. That’s because we need 20 days to calculate the first moving average, therefore the first 19 days are essentially blank.

Hide Non-trading Days

Stock markets close on weekends and holidays, so there’s no data for those periods. The above chart looks all fine but you kind of see small gaps on the bars at the bottom. If we zoom in more, you’ll see them more clearly.

Zoomed in chart with gapsZoomed in chart with gaps

Thanks Dan for suggesting this correction! Let’s now fix this.

Plotly charts have a rangebreaks attribute that we can use to hide certain time periods. This works on both x-axis and y-axis. Also note this attribute is not unique to the candlestick chart, so you can use it to block off time periods for any type of chart with datetime data.

All we need is to add another update_axes(rangebreaks=[…]) to the above code, just before the fig.show().

  • bounds = [‘sat’, ‘mon’] will hide Saturdays and Sundays
  • bounds = [16, 9.5] will hide between 4pm to 9:30am, which are market closed hours
  • values = [“2021-12-25″,”2022-01-01”] can hide individual days
fig3.update_xaxes(rangebreaks = [
                       dict(bounds=['sat','mon']), # hide weekends
                       #dict(bounds=[16, 9.5], pattern='hour'), # for hourly chart, hide non-trading hours (24hr format)
                       dict(values=["2021-12-25","2022-01-01"]) #hide Xmas and New Year
                                ])

The result is a much smoother graph without gaps.

Zoomed in chart - gaps fixed!Zoomed in chart – gaps fixed!

Save Plotly Chart

We can save our stock chart in HTML form, which means all the interactive features will be retained in the graph.

fig3.write_html(r'C:\Users\jay\Desktop\PythonInOffice\plotly_stock_chart\graph.html')

Dash

Although our graph is interactive, it’s still lacking something. For example, if we want to draw a chart for another stock, we have to change the stock ticker inside the code and re-run it. In other words, our graph is not fully interactive yet. With Dash, we can create a graph that takes stock tickers as input and draw the chart accordingly.


import yfinance as yf
import pandas as pd
import plotly as plotly
import plotly.graph_objects as go
from plotly.subplots import make_subplots

def show_plot(fig, filename):
    plotly.offline.plot(fig, filename=filename)


tsla = yf.Ticker('TSLA')
hist = tsla.history(period='1y')
fig = go.Figure(data=go.Scatter(x=hist.index,y=hist['Close'], mode='lines'))
# 只有線圖
show_plot(fig, 'test.html')

# 線圖+點
fig = go.Figure(data=go.Scatter(x=hist.index,y=hist['Close'], mode='lines+markers'))
show_plot(fig, 'test.html')


# 線圖+點+成交量柱狀圖
fig2 = make_subplots(specs=[[{"secondary_y": True}]])
fig2.add_trace(go.Scatter(x=hist.index,y=hist['Close'],name='Price'),secondary_y=False)
fig2.add_trace(go.Bar(x=hist.index,y=hist['Volume'],name='Volume'),secondary_y=True)
show_plot(fig2, 'test.html')


# 線圖+點+成交量柱狀圖+nornalize
fig2.update_yaxes(range=[0,7000000000],secondary_y=True)
fig2.update_yaxes(visible=False, secondary_y=True)
show_plot(fig2, 'test.html')


# 線圖+點+成交量柱狀圖+蠟燭圖
fig3 = make_subplots(specs=[[{"secondary_y": True}]])
fig3.add_trace(go.Candlestick(x=hist.index,
                              open=hist['Open'],
                              high=hist['High'],
                              low=hist['Low'],
                              close=hist['Close'],
                             ))
show_plot(fig3, 'test.html')


# 成交量柱狀圖+蠟燭圖
fig3.add_trace(go.Bar(x=hist.index, y=hist['Volume'], name='Volume'),secondary_y=True)
fig3.update_layout(xaxis_rangeslider_visible=False)
show_plot(fig3, 'test.html')


fig3.add_trace(go.Scatter(x=hist.index,y=hist['Close'].rolling(window=20).mean(),marker_color='blue',name='20 Day MA'))
fig3.add_trace(go.Bar(x=hist.index, y=hist['Volume'], name='Volume'),secondary_y=True)
fig3.update_layout(title={'text':'TSLA', 'x':0.5})
fig3.update_yaxes(range=[0,1000000000],secondary_y=True)
fig3.update_yaxes(visible=False, secondary_y=True)
fig3.update_layout(xaxis_rangeslider_visible=False)  #hide range slider
show_plot(fig3, 'test.html')

Easy and Interactive Candlestick Charts in Python

https://medium.com/@dannygrovesn7/using-streamlit-and-plotly-to-create-interactive-candlestick-charts-a2a764ad0d8e

import pandas as pd
import yfinance as yf
import plotly.io as pio
import plotly.graph_objects as go
import plotly as plotly
from plotly.subplots import make_subplots

pio.renderers.default = "browser"


def get_candlestick_plot(df: pd.DataFrame, ma1: int, ma2: int, ticker: str):
    """
    Create the candlestick chart with two moving avgs + a plot of the volume
    Parameters
    ----------
    df : pd.DataFrame
        The price dataframe
    ma1 : int
        The length of the first moving average (days)
    ma2 : int
        The length of the second moving average (days)
    ticker : str
        The ticker we are plotting (for the title).
    """

    fig = make_subplots(
        rows=2,
        cols=1,
        shared_xaxes=True,
        vertical_spacing=0.1,
        subplot_titles=(f"{ticker} Stock Price", "Volume Chart"),
        row_width=[0.3, 0.7],
    )

    fig.add_trace(
        go.Candlestick(
            # x=df["Date"],
            x=df.index,
            open=df["Open"],
            high=df["High"],
            low=df["Low"],
            close=df["Close"],
            name="Candlestick chart",
        ),
        row=1,
        col=1,
    )

    fig.add_trace(
        go.Line(x=df.index, y=df[f"{ma1}_ma"], name=f"{ma1} SMA"),
        row=1,
        col=1,
    )

    fig.add_trace(
        go.Line(x=df.index, y=df[f"{ma2}_ma"], name=f"{ma2} SMA"),
        row=1,
        col=1,
    )

    fig.add_trace(
        go.Bar(x=df.index, y=df["Volume"], name="Volume"),
        row=2,
        col=1,
    )

    fig["layout"]["xaxis2"]["title"] = "Date"
    fig["layout"]["yaxis"]["title"] = "Price"
    fig["layout"]["yaxis2"]["title"] = "Volume"

    fig.update_xaxes(
        rangebreaks=[{"bounds": ["sat", "mon"]}],
        rangeslider_visible=False,
    )

    return fig


def show_plot(fig, filename):
    plotly.offline.plot(fig, filename=filename)


if __name__ == "__main__":
    stock = "2630.TW"
    tsla = yf.Ticker(stock)
    df = tsla.history(period="10y")

    df["10_ma"] = df["Close"].rolling(10).mean()
    df["20_ma"] = df["Close"].rolling(20).mean()
    fig = get_candlestick_plot(df[-120:], 10, 20, stock)
    # fig.show()
    show_plot(fig, "tsla.html")

子圖

from plotly.subplots import make_subplots
import plotly
import plotly.graph_objects as go

fig = make_subplots(
    rows=2, cols=2,
    specs=[[{"type": "xy", 'secondary_y': True}, {"type": "polar"}],
           [{"type": "domain"}, {"type": "scene"}]],
    y_title='title'
)
'''
也可以為:specs=[[{'type': 'bar'}, {'type': 'barpolar'}],
                [{'type': 'pie'}, {'type': 'scatter3d'}]]
'''

# 使用 secondary_y 參數選擇該圖相對的 y軸座標
fig.add_trace(go.Bar(y=[2, 3, 1]),
              row=1, col=1, secondary_y=False)
fig.add_trace(go.Scatter(x=[0, 1, 2], y=[4, 10, 7]),
              row=1, col=1, secondary_y=True)

fig.add_trace(go.Barpolar(theta=[0, 45, 90], r=[2, 3, 1]),
              row=1, col=2)

fig.add_trace(go.Pie(values=[2, 3, 1]),
              row=2, col=1)

fig.add_trace(go.Scatter3d(x=[2, 3, 1], y=[0, 0, 0],
                           z=[0.5, 1, 2], mode="lines"),
              row=2, col=2)

fig.update_layout(height=700, showlegend=False)

plotly.offline.plot(fig, filename='test.html')

Plotly 在 HTML 中動態更新圖表的簡單示例程序

from plotly.subplots import make_subplots
import random
import dash
from dash import dcc
from dash import html


# 創建圖表
fig = make_subplots(rows=1, cols=1)
fig.add_scatter(x=[], y=[], mode="lines", name="動態更新圖表")

# 創建 Dash 應用
app = dash.Dash(__name__)

# 定義佈局
app.layout = html.Div(
    children=[
        dcc.Graph(id="live-graph", figure=fig),
        dcc.Interval(id="update-interval", interval=1000, n_intervals=0),  # 每秒更新一次
    ]
)


# 定義回調函數
@app.callback(
    dash.dependencies.Output("live-graph", "figure"),
    [dash.dependencies.Input("update-interval", "n_intervals")],
)
def update_graph(n):
    # 生成隨機數據
    x = list(range(10))
    y = [random.randint(0, 100) for _ in range(10)]

    # 更新圖表數據
    fig.data[0].x = x
    fig.data[0].y = y

    return fig


if __name__ == "__main__":
    app.run_server(debug=True)

了 Plotly 的 Python API 來創建了一個動態更新的折線圖,並將其嵌入到了一個 Dash 應用中。具體來說,我們使用 make_subplots() 函數創建了一個包含一個子圖的圖表對象,並在其中添加了一條折線。然後,我們使用 Dash 提供的 dcc.Graph 組件將這個圖表對象嵌入到了應用的佈局中,並使用 dcc.Interval 組件來定時更新圖表數據。最後,我們使用 app.callback 裝飾器定義了一個回調函數,該函數會在定時器觸發時被調用,並更新圖表的數據。

在運行上面的程序後,我們可以在瀏覽器中訪問 http://localhost:8050/ 來查看動態更新的圖表。每秒鐘,圖表中的數據會更新一次,並自動重繪圖表。

PyO3

https://github.com/PyO3/pyo3

pip install maturin
maturin init

src/lib.rs

maturin develop
# lots of progress output as maturin runs the compilation...
$ python
>>> import string_sum
>>> string_sum.sum_as_string(5, 20)
'25'

dataframe 成 pickle 後寫入 redis, 之後取出在 unpickle

from finlab import data
import redis
import pickle

# Connect to Redis
r = redis.Redis(host='localhost', port=6379, db=11)

# Get the data and save it to Redis
營業利益成長率 = data.get("price:收盤價")
r.set('test', pickle.dumps(營業利益成長率))
with open('test.bin', 'wb') as f:
    pickle.dump(營業利益成長率, f)


with open('test.bin', 'rb') as f:
    new_dict = pickle.load(f)
    print(new_dict)

# Read the data from Redis and unpickle it
unpickled_df = pickle.loads(r.get('test'))

print(unpickled_df)

with open('gg.bin', 'wb') as f:
    pickle.dump(pickle.loads(r.get('test')) , f)

with open('gg.bin', 'rb') as f:
    new_dict = pickle.load(f)
    print(new_dict)

對一個欄位nan 使用 bfill

import pandas as pd
import numpy as np

# 建立範例 DataFrame
df = pd.DataFrame({'A': [1, 2, 3, np.nan, np.nan, 6, np.nan, 8]})

# 針對欄位 A 使用 backfill 填充 NaN 值
df['A'] = df['A'].fillna(method='backfill')

# 印出填充後的 DataFrame
print(df)

兩個不同大小DF 使用 fillna() 方法使用後向填充法填充缺失值

import pandas as pd

# 創建第一個 dataframe
df1 = pd.DataFrame({'IsTrue': [False]},
                   index=pd.to_datetime(['2023-02-28']))

# 創建第二個 dataframe
df2 = pd.DataFrame({'close': [239.0, 241.0, 247.0, 227.5, 231.0]},
                   index=pd.to_datetime(['2023-02-17', '2023-02-20', '2023-02-21', '2023-02-22', '2023-02-23']))

# 將第一個 dataframe 轉換為一列 dataframe,然後與第二個 dataframe 進行合併
df = pd.concat([df2, df1], axis=1)  # 使用 pd.concat() 方法將兩個 dataframe 合併
df.fillna(method='bfill', inplace=True)  # 使用 bfill 方法填充缺失值
df.dropna(how="any", inplace=True)  # 使用 dropna 方法刪除仍存在的 NaN 值

print(df)

concat 函數將兩個 DataFrame 以 df1 為主,然後指定 axis=1 將 df2 以欄位的方式合併至 df1

import pandas as pd

# 創建 True/False 值 DataFrame
df1 = pd.DataFrame({'is_buy': [True, False, True, False]}, 
                   index=pd.to_datetime(['2022-01-01', '2022-01-02', '2022-01-03', '2022-01-04']))

# 創建均價 DataFrame(日期為 2022-01-01 和 2022-01-03)
df2 = pd.DataFrame({'mean_price': [100, 300]}, 
                   index=pd.to_datetime(['2022-01-01', '2022-01-03']))

# 將 df1 和 df2 以欄位的方式合併
df_merged = pd.concat([df1, df2.reindex(df1.index)], axis=1)

# 刪除含有 NaN 值的列
df_merged = df_merged.dropna(how='any')

print(df_merged)

資料科學與分析譯文集

pd.options.display.float_format = lambda x: "%.2f" % x

Python量化交易實戰之使用Resample函數轉換“日K”數據

https://walkonnet.com/archives/138620

使用Resample函數轉換時間序列

一、什麼是resample函數?

它是Python數據分析庫Pandas的方法函數。

它主要用於轉換時間序列的頻次。可以做一些統計匯總的工作。

什麼叫轉換時間序列的頻次呢?

比如說股票的日k和周k,

假設我隻能獲取到股票日K的數據,比如說11月1號到11月5號,那怎麼樣將它轉換為以周為單位的K線呢?

日期週期開盤價收盤價最高價最低價
11月1號週一1.111.111.111.12
11月2號週二1.121.121.111.12
11月3號週三1.131.131.111.12
11月4號週四1.151.141.111.12
11月5號週五1.141.151.111.12

首先我們要明確,周K的開盤、收盤、最高、最低是什麼。每週的開盤價是當周第一天的開盤價,收盤價是當周最後一天的收盤價,它的最高價是這周最高的價格,最低價是本週所有最低價中最低的價格。所以你去看炒股平臺,它的周k都是以週五的交易日為記錄的時間點位置。開盤、收盤、最高、最低是按照我剛剛講解的這個規則來計算的。至於月K、年K的選取規則也是一樣的。月K的週期是一個月,年K的週期是一年。

這個計算準確性你也可以通過網上的數據進行驗證。這個計算規則,包括開盤、收盤、最高、最低的計算,收拾resample函數可以做到的事情。此外Resample還有個功能,就是做統計匯總,比如說我想計算一支股票總的周成交量,就可以使用Resample.sum函數去把週一到週五的成交量加起來。

為瞭方便大傢記憶 ,你也可以把resample理解為Excel表格中的透視表功能。你可以按照日期做各種篩選和匯總統計的。最重要的是他可以按照日期。

二、實戰Resample函數

因為這2節課還是一些比較基礎的部分,所以還沒有做模塊化的內容。

我們會在創建股票數據庫的時候 來做真正的模塊化的工作。到這裡都是初級的腳本的形式。先提前說下。

1.日K 轉換為 周K

1.1函數文檔學習

谷歌搜索Pandas Resample:第一個鏈接就是這個函數的官方文檔

https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.resample.html

這裡有介紹:Resample是屬於Pandas DataFrame下面的方法。這裡有關於參數的解釋。

這裡我們隻對2個常用參數講解,一個是rule,另一個是closed。

  • rule表示的是你放一個什麼樣的週期性指標在裡面,用m代表Month,Y代表Year,w代表Week,
  • closed代表你取哪一個分界線,舉例來說,比如說我把日k轉換為周k,到底我是取週一為分界線還是週五為分界線呢?這就是通過closed來確定的。

這裡有它的例子:

>>>index = pd.date_range('1/1/2000', periods=9, freq='T')
>>>series = pd.Series(range(9), index=index)
>>>series
2000-01-01 00:00:00    0
2000-01-01 00:01:00    1
2000-01-01 00:02:00    2
2000-01-01 00:03:00    3
2000-01-01 00:04:00    4
2000-01-01 00:05:00    5
2000-01-01 00:06:00    6
2000-01-01 00:07:00    7
2000-01-01 00:08:00    8
Freq: T, dtype: int64

這裡首先創建瞭一個時間序列的DataFrame,就是這個series變量。你可以理解為它是一個隻有一個字段的表格樣式。接著往下看:

>>>series.resample('3T').sum()
2000-01-01 00:00:00     3
2000-01-01 00:03:00    12
2000-01-01 00:06:00    21
Freq: 3T, dtype: int64

這裡使用瞭Resample方法,3T就是3分鐘,T表示分鐘。sum()就是匯總,也就是針對這一列數據進行匯總。

也就是說,每3分鐘統計依次。註意到,這個時間序列匯總的時間取的值是3分鐘的第一分鐘。如果我想取時間週期的最後一分鐘,可以將label的值改為“right”:

>>>series.resample('3T', label='right').sum()
2000-01-01 00:03:00     3
2000-01-01 00:06:00    12
2000-01-01 00:09:00    21
Freq: 3T, dtype: int64

1.2實戰

獲取日K真實的數據:

#獲取日k
df = get_price("000001.XSHG", end_date='2021-05-30 14:00:00',count=20, frequency='1d', fields=['open','close','high','low','volume','money'])  
print(df)

可以看到獲取到瞭4月28號5月28號的所有數據。為瞭更方便理解 我們再添加一列數據,就是當前日期是星期幾的列。

#獲取日k
df = get_price("000001.XSHG", end_date='2021-05-30 14:00:00',count=20, frequency='1d', fields=['open','close','high','low','volume','money'])  
df['weekday']=df.index.weekday
print(df)

這裡0代表週一,這裡如何轉換為按“”統計呢

#獲取周k
import pandas as pd
df_week = pd.DataFrame()
df_week = df['open'].resample('W').first()
print(df_week)

可以看到這裡的2021-05-30是一個禮拜的最後一天。它對應的開盤價確實是這個數字。說明我們計算的周K數據是正確的。

收盤價就是每週收盤價最後一天的數據。

最高價就是每週收盤價的最大值。

最低價就是每週收盤價的最小值。

#獲取周k
import pandas as pd
df_week = pd.DataFrame()
df_week['open'] = df['open'].resample('W').first()
df_week['close'] = df['close'].resample('W').last()
df_week['high'] = df['high'].resample('W').max()
df_week['low'] = df['low'].resample('W').min()
print(df_week)

對比數據,close是最後一天的收盤價的數據。high是當前周的每天的最高價的最高價。low是當前周的每天的最低價的最低價。

我們通過不到10行代碼就能將日K的數據轉換為周K的數據。

2.匯總統計功能(統計月成交量、成交額)

匯總成交量和成交額

我想要把volume(成交量)和money(成交額)轉換為總成交量總成交額

#獲取周k
import pandas as pd
df_week = pd.DataFrame()
df_week['open'] = df['open'].resample('W').first()
df_week['close'] = df['close'].resample('W').last()
df_week['high'] = df['high'].resample('W').max()
df_week['low'] = df['low'].resample('W').min()
df_week['volume(sum)'] = df['volume'].resample('W').sum()
df_week['money(sum)'] = df['money'].resample('W').sum()
print(df_week)

3.日K 轉換為 月K

假設我有一年的數據,如果想轉換為月K應該怎麼轉?

隻需要改2個地方:

  • 添加start_date獲取到一整年的數據
  • resample的參數改為M即可,M代表Month
#獲取日k
df = get_price("000001.XSHG", end_date='2021-05-30 14:00:00', start_date='2020-05-30', frequency='1d', fields=['open','close','high','low','volume','money'])  
df['weekday']=df.index.weekday
print(df)

#獲取周k
import pandas as pd
df_week = pd.DataFrame()
df_week['open'] = df['open'].resample('M').first()
df_week['close'] = df['close'].resample('M').last()
df_week['high'] = df['high'].resample('M').max()
df_week['low'] = df['low'].resample('M').min()
print(df_week)

以上就是Python量化交易實戰之使用Resample函數轉換“日K”數據的詳細內容,更多關於Python Resample函數轉換“日K”數據的資料請關註WalkonNet其它相關文章!

推薦閱讀:


pandas shift sum

import pandas as pd
import numpy

df = pd.DataFrame(numpy.random.randint(0, 10, (10, 2)), columns=['a','b'])
df['c'] = df.b.rolling(window = 3).sum().shift()
print(df)
import pandas as pd

df = pd.DataFrame(
    {
        "Name": [
            "A",
            "B",
            "C",
            "D",
            "E",
            "F",
            "G",
            "H",
            "I",
            "J",
            "K",
            "L",
            "M",
            "N",
            "O",
        ],
        "Sex": [
            "M",
            "M",
            "M",
            "F",
            "F",
            "M",
            "F",
            "M",
            "F",
            "M",
            "M",
            "M",
            "F",
            "M",
            "M",
        ],
        "Age": [38, 28, 31, 34, 28, 28, 36, 33, 22, 39, 22, 24, 31, 29, 22],
        "Height": [
            1.74,
            1.51,
            1.67,
            1.87,
            1.8,
            1.51,
            1.85,
            1.89,
            1.81,
            1.72,
            1.75,
            1.64,
            1.9,
            1.62,
            1.61,
        ],
        "Weight": [45, 63, 39, 45, 67, 66, 53, 45, 72, 46, 58, 44, 73, 70, 51],
    }
)

df_eg1 = df.copy()
# 第 1 個用法
def BMI_1(r):
    return round(r["Weight"] / (r["Height"] ** 2), ndigits=2)


df_eg1["BMI_apply1"] = df_eg1.apply(BMI_1, axis=1)
# 第 2 個用法
def BMI_2(weight, height):
    return round(weight / (height ** 2), ndigits=2)


df_eg1["BMI_apply2"] = df_eg1.apply(lambda r: BMI_2(r["Weight"], r["Height"]), axis=1)
print(df_eg1)
import pandas as pd

def sum(x, y, z, m, df):
    for index, row in df.iterrows():
        print(row)
    return (x + y + z) * m


df = pd.DataFrame({'A': [1, 2], 'B': [10, 20]})
df1 = df.apply(sum, args=(1, 2), m=10, df=df)
print(df1)

import pandas as pd


def apply_func(tsC, tsD):
    return tsC.mean() + tsD.mean()


dates = pd.date_range("20130101", periods=13, freq="D")
df = pd.DataFrame(
    {
        "C": [1, 9, 2, 4, 5, -1, 6, 9, 3, 5, 10, -3, -5],
        "D": [3, 8, 2, 6, 9, 1, 26, 89, 4, 2, 1, -13, 75],
    },
    index=dates,
)
df.index.name = "datetime"
print(df)

df["rmean"] = (
    df["C"]
    .rolling(window=3)
    .apply(lambda x: apply_func(df.loc[x.index, "C"], df.loc[x.index, "D"]))
)

print(df)
stringList = ["252.007", "546.658", "252.108"]
paramValue = ["252.017", "546.658", "252.008"]

def compareList(l1, l2):
    return [i>j for i, j in zip(l1, l2)]

print(compareList(stringList, paramValue)) 
import pandas as pd
import numpy


def test(data):
    return sum(data)


df = pd.DataFrame(numpy.random.randint(0, 10, (10, 2)), columns=["a", "b"])
# print(df)
c = df.b.rolling(window=3).apply(test).shift()
# df['c'] = df.b.rolling(window = 3).sum().shift(1)
# df['d'] = df.b.rolling(window = 3).sum().shift(2)
print(df, "\n", c)
from numpy_ext import rolling_apply
import pandas as pd
import numpy


def test(a, b):
    return sum(a) + sum(b)


df = pd.DataFrame(numpy.random.randint(0, 10, (10, 2)), columns=["a", "b"])
df["c"] = rolling_apply(test, 3, df.a.values, df.b.values)
df["c"] = df["c"].shift()
print(df, type(df["c"]))

Resample

# https://medium.com/uxai/%E9%87%8F%E5%8C%96%E6%8A%95%E8%B3%87-ai-for-trading-1-%E7%8D%B2%E5%8F%96%E5%B8%82%E5%A0%B4%E8%B3%87%E6%96%99-109791cde0f5
import yfinance as yf
import pandas as pd


def get_info_on_stock(ticker):
    stock = yf.Ticker(ticker)
    # 拿上市至今的收盤價
    hist_all = stock.history(period="max")["Close"]
    # 拿近 30 天的所有資料
    hist_30 = stock.history(period="30d")
    return hist_all


def get_info_on_stocks(track_list):
    df = pd.DataFrame()
    for stock in track_list:
        stock_info = yf.Ticker(stock)
        # 拿近 10 天的資料
        hist = stock_info.history(period="360d")
        # 做一些簡單的處理後把 dataframe 接起來
        hist["Stock_id"] = stock
        hist["Date"] = hist.index
        hist = hist[
            [
                "Date",
                "Stock_id",
                "Open",
                "High",
                "Low",
                "Close",
                "Volume",
                "Dividends",
                "Stock Splits",
            ]
        ]
        df = pd.concat([df, hist])
    return df.set_index([pd.Index([i for i in range(len(df))])]).round(1)


if __name__ == "__main__":
    data = get_info_on_stock("2330.TW")
    print(data)

    # 選定我們要比較的公司
    track_list = ["2330.TW", "2303.TW"]
    df = get_info_on_stocks(track_list)
    print(df, type(df))

    # 接下來我們可以做一些簡單的計算,利用 pandas 內建的函數來看兩者資訊的平均 (mean):
    df_mean = df.groupby("Stock_id").mean()
    print(df_mean)

    # 前面的圖可以看到我們是將同一個時間的兩個公司資訊堆疊起來,接下來我們可以試著用另一種表示方式來做比較:
    open_prices = df.pivot(index="Date", columns="Stock_id", values="Open")
    high_prices = df.pivot(index="Date", columns="Stock_id", values="High")
    low_prices = df.pivot(index="Date", columns="Stock_id", values="Low")
    close_prices = df.pivot(index="Date", columns="Stock_id", values="Close")
    volume = df.pivot(index="Date", columns="Stock_id", values="Volume")
    print(close_prices.mean())
    print(close_prices)
    # print(close_prices['2303.TW'])
    df_month_open = open_prices.resample("M").first()
    df_month_high = high_prices.resample("M").max()
    df_month_low = low_prices.resample("M").min()
    df_month_close = close_prices.resample("M").last()
    print(df_month_open)
from finlab import data
import datetime
import numpy as np
import pandas as pd
import finlab
import functools


class MyFinlabDataFrame(pd.DataFrame):
    """回測語法糖
    除了使用熟悉的 Pandas 語法外,我們也提供很多語法糖,讓大家開發程式時,可以用簡易的語法完成複雜的功能,讓開發策略更簡潔!
    我們將所有的語法糖包裹在 `MyFinlabDataFrame` 中,用起來跟 `pd.DataFrame` 一樣,但是多了很多功能!
    只要使用 `finlab.data.get()` 所獲得的資料,皆為 `MyFinlabDataFrame` 格式,
    接下來我們就來看看, `MyFinlabDataFrame` 有哪些好用的語法糖吧!

    當資料日期沒有對齊(例如: 財報 vs 收盤價 vs 月報)時,在使用以下運算符號:`+`, `-`, `*`, `/`, `>`, `>=`, `==`, `<`, `<=`, `&`, `|`, `~`,不需要先將資料對齊,因為 `MyFinlabDataFrame` 會自動幫你處理,以下是示意圖。

    <img src="https://i.ibb.co/pQr5yx5/Screen-Shot-2021-10-26-at-5-32-44-AM.png" alt="Screen-Shot-2021-10-26-at-5-32-44-AM">

    以下是範例:`cond1` 與 `cond2` 分別為「每天」,和「每季」的資料,假如要取交集的時間,可以用以下語法:

    ```py
    from finlab import data
    # 取得 MyFinlabDataFrame
    close = data.get('price:收盤價')
    roa = data.get('fundamental_features:ROA稅後息前')

    # 運算兩個選股條件交集
    cond1 = close > 37
    cond2 = roa > 0
    cond_1_2 = cond1 & cond2

    擷取 1101 臺泥 的訊號如下圖,可以看到 `cond1` 跟 `cond2` 訊號的頻率雖然不相同,但是由於 `cond1` 跟 `cond2` 是 `MyFinlabDataFrame`,所以可以直接取交集,而不用處理資料頻率對齊的問題。
    <br />
    <img src="https://i.ibb.co/m9chXSQ/imageconds.png" alt="imageconds">
    
    總結來說,MyFinlabDataFrame 與一般 dataframe 唯二不同之處:
    1. 多了一些 method,如`df.is_largest()`, `df.sustain()`...等。
    2. 在做四則運算、不等式運算前,會將 df1、df2 的 index 取聯集,column 取交集。
    """
    
    @property
    def _constructor(self):
        return MyFinlabDataFrame
    
    @staticmethod
    def reshape(df1, df2):
    
        isfdf1 = isinstance(df1, MyFinlabDataFrame)
        isfdf2 = isinstance(df2, MyFinlabDataFrame)
        isdf1 = isinstance(df1, pd.DataFrame)
        isdf2 = isinstance(df2, pd.DataFrame)
    
        both_are_dataframe = (isfdf1 + isdf1) * (isfdf2 + isdf2) != 0
    
        d1_index_freq = df1.get_index_str_frequency() if isfdf1 else None
        d2_index_freq = df2.get_index_str_frequency() if isfdf2 else None
    
        if (
            (d1_index_freq or d2_index_freq)
            and (d1_index_freq != d2_index_freq)
            and both_are_dataframe
        ):
    
            df1 = df1.index_str_to_date() if isfdf1 else df1
            df2 = df2.index_str_to_date() if isfdf2 else df2
    
        if isinstance(df2, pd.Series):
            df2 = pd.DataFrame({c: df2 for c in df1.columns})
    
        if both_are_dataframe:
            index = df1.index.union(df2.index)
            columns = df1.columns.intersection(df2.columns)
    
            if len(df1.index) * len(df2.index) != 0:
                index_start = max(df1.index[0], df2.index[0])
                index = [t for t in index if index_start <= t]
    
            return (
                df1.reindex(index=index, method="ffill")[columns],
                df2.reindex(index=index, method="ffill")[columns],
            )
        else:
            return df1, df2
    
    def __lt__(self, other):
        df1, df2 = self.reshape(self, other)
        return pd.DataFrame.__lt__(df1, df2)
    
    def __gt__(self, other):
        df1, df2 = self.reshape(self, other)
        return pd.DataFrame.__gt__(df1, df2)
    
    def __le__(self, other):
        df1, df2 = self.reshape(self, other)
        return pd.DataFrame.__le__(df1, df2)
    
    def __ge__(self, other):
        df1, df2 = self.reshape(self, other)
        return pd.DataFrame.__ge__(df1, df2)
    
    def __eq__(self, other):
        df1, df2 = self.reshape(self, other)
        return pd.DataFrame.__eq__(df1, df2)
    
    def __ne__(self, other):
        df1, df2 = self.reshape(self, other)
        return pd.DataFrame.__ne__(df1, df2)
    
    def __sub__(self, other):
        df1, df2 = self.reshape(self, other)
        return pd.DataFrame.__sub__(df1, df2)
    
    def __add__(self, other):
        df1, df2 = self.reshape(self, other)
        return pd.DataFrame.__add__(df1, df2)
    
    def __mul__(self, other):
        df1, df2 = self.reshape(self, other)
        return pd.DataFrame.__mul__(df1, df2)
    
    def __truediv__(self, other):
        df1, df2 = self.reshape(self, other)
        return pd.DataFrame.__truediv__(df1, df2)
    
    def __rshift__(self, other):
        return self.shift(-other)
    
    def __lshift__(self, other):
        return self.shift(other)
    
    def __and__(self, other):
        df1, df2 = self.reshape(self, other)
        return pd.DataFrame.__and__(df1, df2)
    
    def __or__(self, other):
        df1, df2 = self.reshape(self, other)
        return pd.DataFrame.__or__(df1, df2)
    
    def __getitem__(self, other):
        df1, df2 = self.reshape(self, other)
        return pd.DataFrame.__getitem__(df1, df2)
    
    def index_str_to_date(self):
        """財務月季報索引格式轉換
    
        將以下資料的索引轉換成datetime格式:
    
        月營收 (ex:2022-M1) 從文字格式轉為公告截止日。
    
        財務季報 (ex:2022-Q1) 從文字格式轉為財報電子檔資料上傳日。
    
        通常使用情境為對不同週期的dataframe做reindex,常用於以公告截止日作為訊號產生日。
    
        Returns:
          (pd.DataFrame): data
        Examples:
    
            ```py
            data.get('monthly_revenue:當月營收').index_str_to_date()
            data.get('financial_statement:現金及約當現金').index_str_to_date()
  """
    if len(self.index) == 0 or not isinstance(self.index[0], str):
        return self

    if self.index[0].find("M") != -1:
        return self._index_str_to_date_month()
    elif self.index[0].find("Q") != -1:
        return self._index_str_to_date_season()

    return self

@staticmethod
def to_business_day(date):
    def skip_weekend(d):
        add_days = {5: 2, 6: 1}
        wd = d.weekday()
        if wd in add_days:
            d += datetime.timedelta(days=add_days[wd])
        return d

    close = data.get("price:收盤價")
    return (
        pd.Series(date)
        .apply(
            lambda d: skip_weekend(d)
            if d in close.index or d < close.index[0] or d > close.index[-1]
            else close.loc[d:].index[0]
        )
        .values
    )

def get_index_str_frequency(self):

    if len(self.index) == 0:
        return None

    if not isinstance(self.index[0], str):
        return None

    if (self.index.str.find("M") != -1).all():
        return "month"

    if (self.index.str.find("Q") != -1).all():
        return "season"

    return None

def _index_date_to_str_month(self):

    # index is already str
    if len(self.index) == 0 or not isinstance(self.index[0], pd.Timestamp):
        return self

    index = (self.index - datetime.timedelta(days=30)).strftime("%Y-M%m")
    return MyFinlabDataFrame(self.values, index=index, columns=self.columns)

def _index_str_to_date_month(self):

    # index is already timestamps
    if len(self.index) == 0 or not isinstance(self.index[0], str):
        return self

    if not (self.index.str.find("M") != -1).all():
        logger.warning(
            "MyFinlabDataFrame: invalid index, cannot format index to monthly timestamp."
        )
        return self

    index = (
        pd.to_datetime(self.index, format="%Y-M%m")
        + pd.offsets.MonthBegin()
        + datetime.timedelta(days=9)
    )
    # chinese new year and covid-19 impact monthly revenue deadline
    replacements = {
        datetime.datetime(2020, 2, 10): datetime.datetime(2020, 2, 15),
        datetime.datetime(2021, 2, 10): datetime.datetime(2021, 2, 15),
        datetime.datetime(2022, 2, 10): datetime.datetime(2022, 2, 14),
    }
    replacer = replacements.get
    index = [replacer(n, n) for n in index]

    index = self.to_business_day(index)

    ret = MyFinlabDataFrame(self.values, index=index, columns=self.columns)
    ret.index.name = "date"

    return ret

def _index_date_to_str_season(self):

    # index is already str
    if len(self.index) == 0 or not isinstance(self.index[0], pd.Timestamp):
        return self

    q = (
        self.index.strftime("%m")
        .astype(int)
        .map({5: 1, 8: 2, 9: 2, 10: 3, 11: 3, 3: 4, 4: 4})
    )
    year = self.index.year.copy()
    year -= q == 4
    index = year.astype(str) + "-Q" + q.astype(str)

    return MyFinlabDataFrame(self.values, index=index, columns=self.columns)

def deadline(self):
    """財務季報索引轉換成公告截止日

      將財務季報 (ex:2022Q1) 從文字格式轉為公告截止日的datetime格式,
      通常使用情境為對不同週期的dataframe做reindex,常用於以公告截止日作為訊號產生日。
      Returns:
        (pd.DataFrame): data
      Examples:
          ```py
          data.get('financial_statement:現金及約當現金').deadline()
          ```
    """
    return self._index_str_to_date_season(detail=False)

def _index_str_to_date_season(self, detail=True):

    disclosure_dates = calc_disclosure_dates(detail).reindex_like(self).unstack()

    self.columns.name = "stock_id"

    unstacked = self.unstack()

    ret = (
        pd.DataFrame(
            {"value": unstacked.values, "disclosures": disclosure_dates.values,},
            unstacked.index,
        )
        .reset_index()
        .drop_duplicates(["disclosures", "stock_id"])
        .pivot(index="disclosures", columns="stock_id", values="value")
        .ffill()
        .pipe(lambda df: df.loc[df.index.notna()])
        .pipe(lambda df: MyFinlabDataFrame(df))
        .rename_axis("date")
    )

    if not detail:
        ret.index = self.to_business_day(ret.index)

    return ret

def average(self, n):
    """取 n 筆移動平均

    若股票在時間窗格內,有 N/2 筆 NaN,則會產生 NaN。
    Args:
      n (positive-int): 設定移動窗格數。
    Returns:
      (pd.DataFrame): data
    Examples:
        股價在均線之上
        ```py
        from finlab import data
        close = data.get('price:收盤價')
        sma = close.average(10)
        cond = close > sma
        ```
        只需要簡單的語法,就可以將其中一部分的訊號繪製出來檢查:
        ```py
        import matplotlib.pyplot as plt

        close.loc['2021', '2330'].plot()
        sma.loc['2021', '2330'].plot()
        cond.loc['2021', '2330'].mul(20).add(500).plot()

        plt.legend(['close', 'sma', 'cond'])
        ```
        <img src="https://i.ibb.co/Mg1P85y/sma.png" alt="sma">
    """
    return self.rolling(n, min_periods=int(n / 2)).mean()

def is_largest(self, n):
    """取每列前 n 筆大的數值

    若符合 `True` ,反之為 `False` 。用來篩選每天數值最大的股票。

    <img src="https://i.ibb.co/8rh3tbt/is-largest.png" alt="is-largest">
    Args:
      n (positive-int): 設定每列前 n 筆大的數值。
    Returns:
      (pd.DataFrame): data
    Examples:
        每季 ROA 前 10 名的股票
        ```py
        from finlab import data

        roa = data.get('fundamental_features:ROA稅後息前')
        good_stocks = roa.is_largest(10)
        ```
    """
    return (
        self.astype(float)
        .apply(lambda s: s.nlargest(n), axis=1)
        .reindex_like(self)
        .notna()
    )

def is_smallest(self, n):
    """取每列前 n 筆小的數值

    若符合 `True` ,反之為 `False` 。用來篩選每天數值最小的股票。
    Args:
      n (positive-int): 設定每列前 n 筆小的數值。
    Returns:
      (pd.DataFrame): data
    Examples:
        股價淨值比最小的 10 檔股票
        ```py
        from finlab import data

        pb = data.get('price_earning_ratio:股價淨值比')
        cheap_stocks = pb.is_smallest(10)
        ```
    """
    return (
        self.astype(float)
        .apply(lambda s: s.nsmallest(n), axis=1)
        .reindex_like(self)
        .notna()
    )

def is_entry(self):
    """進場點

    取進場訊號點,若符合條件的值則為True,反之為False。
    Returns:
      (pd.DataFrame): data
    Examples:
      策略為每日收盤價前10高,取進場點。
        ```py
        from finlab import data
        data.get('price:收盤價').is_largest(10).is_entry()
        ```
    """
    return self & ~self.shift(fill_value=False)

def is_exit(self):
    """出場點

    取出場訊號點,若符合條件的值則為 True,反之為 False。
    Returns:
      (pd.DataFrame): data
    Examples:
      策略為每日收盤價前10高,取出場點。
        ```py
        from finlab import data
        data.get('price:收盤價').is_largest(10).is_exit()
        ```
    """
    return ~self & self.shift(fill_value=False)

def rise(self, n=1):
    """數值上升中

    取是否比前第n筆高,若符合條件的值則為True,反之為False。
    <img src="https://i.ibb.co/Y72bN5v/Screen-Shot-2021-10-26-at-6-43-41-AM.png" alt="Screen-Shot-2021-10-26-at-6-43-41-AM">
    Args:
      n (positive-int): 設定比較前第n筆高。
    Returns:
      (pd.DataFrame): data
    Examples:
        收盤價是否高於10日前股價
        ```py
        from finlab import data
        data.get('price:收盤價').rise(10)
        ```
    """
    return self > self.shift(n)

def fall(self, n=1):
    """數值下降中

    取是否比前第n筆低,若符合條件的值則為True,反之為False。
    <img src="https://i.ibb.co/Y72bN5v/Screen-Shot-2021-10-26-at-6-43-41-AM.png" alt="Screen-Shot-2021-10-26-at-6-43-41-AM">
    Args:
      n (positive-int): 設定比較前第n筆低。
    Returns:
      (pd.DataFrame): data
    Examples:
        收盤價是否低於10日前股價
        ```py
        from finlab import data
        data.get('price:收盤價').fall(10)
        ```
    """
    return self < self.shift(n)

def groupby_category(self):
    """資料按產業分群

    類似 `pd.DataFrame.groupby()`的處理效果。
    Returns:
      (pd.DataFrame): data
    Examples:
      半導體平均股價淨值比時間序列
        ```py
        from finlab import data
        pe = data.get('price_earning_ratio:股價淨值比')
        pe.groupby_category().mean()['半導體'].plot()
        ```
        <img src="https://i.ibb.co/Tq2fKBp/pbmean.png" alt="pbmean">

        全球 2020 量化寬鬆加上晶片短缺,使得半導體股價淨值比衝高。
    """
    categories = data.get("security_categories")
    cat = categories.set_index("stock_id").category.to_dict()
    org_set = set(cat.values())
    set_remove_illegal = set(
        o for o in org_set if isinstance(o, str) and o != "nan"
    )
    set_remove_illegal

    refine_cat = {}
    for s, c in cat.items():
        if c == None or c == "nan":
            refine_cat[s] = "其他"
            continue

        if c == "電腦及週邊":
            refine_cat[s] = "電腦及週邊設備業"
            continue

        if c[-1] == "業" and c[:-1] in set_remove_illegal:
            refine_cat[s] = c[:-1]
        else:
            refine_cat[s] = c

    col_categories = pd.Series(
        self.columns.map(lambda s: refine_cat[s] if s in cat else "其他")
    )

    return self.groupby(col_categories.values, axis=1)

def entry_price(self, trade_at="close"):

    signal = self.is_entry()
    adj = (
        data.get("etl:adj_close")
        if trade_at == "close"
        else data.get("etl:adj_open")
    )
    adj, signal = adj.reshape(adj.loc[signal.index[0] : signal.index[-1]], signal)
    return adj.bfill()[signal.shift(fill_value=False)].ffill()

def sustain(self, nwindow, nsatisfy=None):
    """持續 N 天滿足條件

    取移動 nwindow 筆加總大於等於nsatisfy,若符合條件的值則為True,反之為False。

    Args:
      nwindow (positive-int): 設定移動窗格。
      nsatisfy (positive-int): 設定移動窗格計算後最低滿足數值。
    Returns:
      (pd.DataFrame): data
    Examples:
        收盤價是否連兩日上漲
        ```py
        from finlab import data
        data.get('price:收盤價').rise().sustain(2)
        ```
    """
    nsatisfy = nsatisfy or nwindow
    return self.rolling(nwindow).sum() >= nsatisfy

def industry_rank(self, categories=None):
    """計算產業 ranking 排名,0 代表產業內最低,1 代表產業內最高
    Args:
      categories (list of str): 欲考慮的產業,ex: ['貿易百貨', '雲端運算'],預設為全產業,請參考 `data.get('security_industry_themes')` 中的產業項目。
    Examples:
        本意比產業排名分數
        ```py
        from finlab import data

        pe = data.get('price_earning_ratio:本益比')
        pe_rank = pe.industry_rank()
        print(pe_rank)
        ```
    """

    themes = (
        data.get("security_industry_themes")
        .copy()  # 複製
        .assign(
            category=lambda self: self.category.apply(lambda s: eval(s))
        )  # 從文字格式轉成陣列格
        .explode("category")  # 展開資料
    )

    categories = categories or set(
        themes.category[themes.category.str.find(":") == -1]
    )

    def calc_rank(ind):
        stock_ids = themes.stock_id[themes.category == ind]
        return self[stock_ids].pipe(lambda self: self.rank(axis=1, pct=True))

    return (
        pd.concat([calc_rank(ind) for ind in categories], axis=1)
        .groupby(level=0, axis=1)
        .mean()
    )

def quantile_row(self, c):
    """股票當天數值分位數

    取得每列c定分位數的值。
    Args:
      c (positive-int): 設定每列 n 定分位數的值。
    Returns:
      (pd.DataFrame): data
    Examples:
        取每日股價前90%分位數
        ```py
        from finlab import data
        data.get('price:收盤價').quantile_row(0.9)
        ```
    """
    s = self.quantile(c, axis=1)
    return s

def exit_when(self, exit):

    df, exit = self.reshape(self, exit)

    df.fillna(False, inplace=True)
    exit.fillna(False, inplace=True)

    entry_signal = df.is_entry()
    exit_signal = df.is_exit()
    exit_signal |= exit

    # build position using entry_signal and exit_signal
    position = pd.DataFrame(np.nan, index=df.index, columns=df.columns)
    position[entry_signal] = 1
    position[exit_signal] = 0

    position.ffill(inplace=True)
    position = position == 1
    position.fillna(False)
    return position

def hold_until(
    self,
    exit,
    nstocks_limit=None,
    stop_loss=-np.inf,
    take_profit=np.inf,
    trade_at="close",
    rank=None,
):
    """訊號進出場

    這大概是所有策略撰寫中,最重要的語法糖,上述語法中 `entries` 為進場訊號,而 `exits` 是出場訊號。所以 `entries.hold_until(exits)` ,就是進場訊號為 `True` 時,買入並持有該檔股票,直到出場訊號為 `True ` 則賣出。
    <img src="https://i.ibb.co/PCt4hPd/Screen-Shot-2021-10-26-at-6-35-05-AM.png" alt="Screen-Shot-2021-10-26-at-6-35-05-AM">
    此函式有很多細部設定,可以讓你最多選擇 N 檔股票做輪動。另外,當超過 N 檔進場訊號發生,也可以按照客製化的排序,選擇優先選入的股票。最後,可以設定價格波動當輪動訊號,來增加出場的時機點。

    Args:
      exit (pd.Dataframe): 出場訊號。
      nstocks_limit (int)`: 輪動檔數上限,預設為None。
      stop_loss (float): 價格波動輪動訊號,預設為None,不生成輪動訊號。範例:0.1,代表成本價下跌 10% 時產生出場訊號。
      take_profit (float): 價格波動輪動訊號,預設為None,不生成輪動訊號。範例:0.1,代表成本價上漲 10% 時產生出場訊號。
      trade_at (str): 價格波動輪動訊號參考價,預設為'close'。可選 `close` 或 `open`。
      rank (pd.Dataframe): 當天進場訊號數量超過 nstocks_limit 時,以 rank 數值越大的股票優先進場。
    Returns:
      (pd.DataFrame): data
    Examples:
        價格 > 20 日均線入場, 價格 < 60 日均線出場,最多持有10檔,超過 10 個進場訊號,則以股價淨值比小的股票優先選入。

        ```python
        from finlab import data
        from finlab.backtest import sim

        close = data.get('price:收盤價')
        pb = data.get('price_earning_ratio:股價淨值比')

        sma20 = close.average(20)
        sma60 = close.average(60)

        entries = close > sma20
        exits = close < sma60

        #pb前10小的標的做輪動
        position = entries.hold_until(exits, nstocks_limit=10, rank=-pb)
        sim(position)
        ```
    """
    if nstocks_limit is None:
        nstocks_limit = len(self.columns)

    union_index = self.index.union(exit.index)
    intersect_col = self.columns.intersection(exit.columns)

    if stop_loss != -np.inf or take_profit != np.inf:
        price = data.get(f"etl:adj_{trade_at}")
        union_index = union_index.union(
            price.loc[union_index[0] : union_index[-1]].index
        )
        intersect_col = intersect_col.intersection(price.columns)
    else:
        price = pd.DataFrame()

    if rank is not None:
        union_index = union_index.union(rank.index)
        intersect_col = intersect_col.intersection(rank.columns)

    entry = (
        self.reindex(union_index, columns=intersect_col, method="ffill")
        .ffill()
        .fillna(False)
    )
    exit = (
        exit.reindex(union_index, columns=intersect_col, method="ffill")
        .ffill()
        .fillna(False)
    )

    if price is not None:
        price = price.reindex(union_index, columns=intersect_col, method="ffill")

    if rank is not None:
        rank = rank.reindex(union_index, columns=intersect_col, method="ffill")
    else:
        rank = pd.DataFrame(1, index=union_index, columns=intersect_col)

    max_rank = rank.max().max()
    min_rank = rank.min().min()
    rank = (rank - min_rank) / (max_rank - min_rank)
    rank.fillna(0, inplace=True)

    def rotate_stocks(
        ret,
        entry,
        exit,
        nstocks_limit,
        stop_loss=-np.inf,
        take_profit=np.inf,
        price=None,
        ranking=None,
    ):

        nstocks = 0

        ret[0][np.argsort(entry[0])[-nstocks_limit:]] = 1
        ret[0][exit[0] == 1] = 0
        ret[0][entry[0] == 0] = 0

        entry_price = np.empty(entry.shape[1])
        entry_price[:] = np.nan

        for i in range(1, entry.shape[0]):

            # regitser entry price
            if stop_loss != -np.inf or take_profit != np.inf:
                is_entry = (ret[i - 2] == 0) if i > 1 else (ret[i - 1] == 1)

                is_waiting_for_entry = np.isnan(entry_price) & (ret[i - 1] == 1)

                is_entry |= is_waiting_for_entry

                entry_price[is_entry == 1] = price[i][is_entry == 1]

                # check stop_loss and take_profit
                returns = price[i] / entry_price
                stop = (returns > 1 + abs(take_profit)) | (
                    returns < 1 - abs(stop_loss)
                )
                exit[i] |= stop

            # run signal
            rank = entry[i] * ranking[i] + ret[i - 1] * 3
            rank[exit[i] == 1] = -1
            rank[(entry[i] == 0) & (ret[i - 1] == 0)] = -1

            ret[i][np.argsort(rank)[-nstocks_limit:]] = 1
            ret[i][rank == -1] = 0

        return ret

    ret = pd.DataFrame(0, index=entry.index, columns=entry.columns)
    ret = rotate_stocks(
        ret.values,
        entry.astype(int).values,
        exit.astype(int).values,
        nstocks_limit,
        stop_loss,
        take_profit,
        price=price.values,
        ranking=rank.values,
    )

    return pd.DataFrame(ret, index=entry.index, columns=entry.columns)

@functools.lru_cache def calc_disclosure_dates(detail=True):

cinfo = data.get("company_basic_info").copy()
cinfo["id"] = cinfo.stock_id.str.split(" ").str[0]
cinfo = cinfo.set_index("id")
cinfo = cinfo[~cinfo.index.duplicated(keep="last")]

def calc_default_disclosure_dates(s):
    sid = s.name
    cat = cinfo.loc[sid].產業類別 if sid in cinfo.index else "etf"
    short_name = cinfo.loc[sid].公司簡稱 if sid in cinfo.index else "etf"

    if cat == "金融業":
        calendar = {
            "1": "-05-15",
            "2": "-08-31",
            "3": "-11-14",
            "4": "-03-31",
        }
    elif cat == "金融保險業":
        calendar = {
            "1": "-04-30",
            "2": "-08-31",
            "3": "-10-31",
            "4": "-03-31",
        }
    elif "KY" in short_name:
        calendar = {
            "old": {"1": "-05-15", "2": "-08-14", "3": "-11-14", "4": "-03-31",},
            "new": {"1": "-05-15", "2": "-08-31", "3": "-11-14", "4": "-03-31",},
        }
    else:
        calendar = {
            "1": "-05-15",
            "2": "-08-14",
            "3": "-11-14",
            "4": "-03-31",
        }
    get_year = (
        lambda year, season: str(year) if int(season) != 4 else str(int(year) + 1)
    )
    ky_policy_check = lambda year: "new" if year >= "2021" else "old"
    return pd.to_datetime(
        s.index.map(
            lambda d: get_year(d[:4], d[-1])
            + calendar[ky_policy_check(d[:4])][d[-1]]
        )
        if "KY" in short_name
        else s.index.map(lambda d: get_year(d[:4], d[-1]) + calendar[d[-1]])
    )

def season_end(s):

    calendar = {
        "1": "-3-31",
        "2": "-6-30",
        "3": "-9-30",
        "4": "-12-31",
    }
    return pd.to_datetime(s.index.map(lambda d: d[:4] + calendar[d[-1]]))

disclosure_dates = data.get("financial_statements_upload_detail:upload_date")
disclosure_dates = disclosure_dates.apply(pd.to_datetime)

financial_season_end = disclosure_dates.apply(season_end)
default_disclosure_dates = disclosure_dates.apply(calc_default_disclosure_dates)

disclosure_dates[
    (disclosure_dates > default_disclosure_dates)
    | (disclosure_dates < financial_season_end)
] = pd.NaT
disclosure_dates[(disclosure_dates.diff() <= datetime.timedelta(days=0))] = pd.NaT
disclosure_dates.loc["2019-Q1", "3167"] = pd.NaT
disclosure_dates.loc["2015-Q1", "5536"] = pd.NaT
disclosure_dates.loc["2018-Q1", "5876"] = pd.NaT

disclosure_dates = disclosure_dates.fillna(default_disclosure_dates)
disclosure_dates.columns.name = "stock_id"

if detail:
    return disclosure_dates
return default_disclosure_dates

if name == "main": # finlab.login("") # close = data.get("price:收盤價") # close = MyFinlabDataFrame(close) # rev = data.get("monthly_revenue:當月營收") # rev = MyFinlabDataFrame(rev)

## 股價創年新高
# cond1 = close == close.rolling(250).max()

## 確認營收底部,近月營收脫離近年穀底(連續3月的"單月營收近12月最小值/近月營收" < 0.8)
# cond4 = ((rev.rolling(12).min()) / (rev) < 0.8).sustain(3)
# print(cond1)
# print(cond4)
# print(cond1 & cond4)

df = MyFinlabDataFrame(
    np.random.randint(0, 100, size=(10, 5)), columns=list("BCDEH")
)
df1 = MyFinlabDataFrame(
    np.random.randint(0, 100, size=(5, 4)), columns=list("ABCD")
)
df = df > 50
df1 = df1 > 50
print(df)
print(df1)
print(df & df1)


## pandas 找出重複的列
```python
import pandas as pd

# Example dataframes
df1 = pd.DataFrame({
    'date': ['2002-02-01', '2002-02-01', '2002-03-01', '2002-03-01', '2002-04-01'],
    'stock_id': [1101, 1101, 1101, 1101, 1101],
    'country': ['Taiwan', 'Taiwan', 'Taiwan', 'Taiwan', 'Taiwan'],
    'revenue': [2200067000, 2200067000, 1404336000, 1404336000, 2028782000],
    'revenue_month': [1, 1, 2, 2, 3],
    'revenue_year': [2002, 2002, 2002, 2002, 2002]
})

df2 = pd.DataFrame({
    'date': ['2023-02-01', '2023-03-01', '2023-03-01', '2023-04-01', '2023-04-01'],
    'stock_id': [1101, 1101, 1101, 1101, 1101],
    'country': ['Taiwan', 'Taiwan', 'Taiwan', 'Taiwan', 'Taiwan'],
    'revenue': [7325221000, 7306069000, 7306069000, 11730367000, 11730367000],
    'revenue_month': [1, 2, 2, 3, 3],
    'revenue_year': [2023, 2023, 2023, 2023, 2023]
})


print(df1, df2)
concatenated = pd.concat([df1, df2], ignore_index=True)
differences = concatenated.drop_duplicates(keep=False)
print(differences)

兩組 dataframe 找出多餘 row 組成 dataframe

import pandas as pd

# create the DataFrame
import pandas as pd

df1 = pd.DataFrame({'A': [1, 2, 3], 'B': [1, 5, 6]})
df2 = pd.DataFrame({'A': [1, 2], 'B': [1, 5]})
df = df1 - df2
print(df1.to_markdown())
print("\n")
print(df2.to_markdown())


result = df1 - df2

indexs = []

for index, row in df.iterrows():
    if row.A != 0 or row.B != 0:
        indexs.append(index)
        

print("\n")
result_df = df1.loc[indexs]
print(result_df.to_markdown())

async def & await 重點整理

sync def & await 使用情境

我直接利用下面這個例子來展示什麼情況下可以使用 asyncawait

import time

def dosomething(i):
    print(f"第 {i} 次開始")
    time.sleep(2)
    print(f"第 {i} 次結束")

if __name__ == "__main__":
    start = time.time()
    for i in range(5):
        dosomething(i+1)
    print(f"time: {time.time() - start} (s)")

執行後應該會像這樣。

第 1 次開始
第 1 次結束
第 2 次開始
第 2 次結束
第 3 次開始
第 3 次結束
第 4 次開始
第 4 次結束
第 5 次開始
第 5 次結束
time: 10.048049688339233 (s)

這非常直覺,因為每次呼叫 dosomething() 時都會等待2秒,等完才會執行下一輪,所以最後執行總時間是10秒相當合理。

img

但仔細想想,如果那2秒是做網路請求或檔案讀寫(IO),這2秒是不需要CPU的,但CPU就只能發呆2秒,痴痴地等待回傳結果,其他什麼事都不能做,豈不是太浪費了嗎!? (學過作業系統的人就知道,絕對不能讓CPU發呆XD)

因此 Python 就有了 asyncio 這個工具,來徹底的利用(X) 榨乾(O) CPU的效能。

我把剛才的例子改成 asyncio 的版本。

我把剛才的例子改成 asyncio 的版本。

import time
import asyncio

async def dosomething(i):
    print(f"第 {i} 次開始")
    await asyncio.sleep(2)
    print(f"第 {i} 次結束")

if __name__ == "__main__":
    start = time.time()
    tasks = [dosomething(i+1) for i in range(5)]
    asyncio.run(asyncio.wait(tasks))
    print(f"time: {time.time() - start} (s)")

執行結果會變成這樣,只需要2秒就結束了!

第 2 次開始
第 1 次開始
第 3 次開始
第 4 次開始
第 5 次開始
第 2 次結束
第 3 次結束
第 5 次結束
第 1 次結束
第 4 次結束
time: 2.011152982711792 (s)

為什麼會這樣呢? 其實 await 就是告訴 CPU 說後面這個函數很慢,不需要等它執行完畢。因此此時 CPU 就可以先跳去執行其他的事情,只需要在這個函數結束時再回來處理就好。這就是為什麼速度會快很多。

img

瞭解 async def & await 使用情境之後,就來說明一些細節吧!

coroutine

首先,async def & await 是 Python 3.5+ 之後才出現的 語法糖,目的是讓 coroutine 之間的調度更加清楚。

那就要先了解什麼是 coroutine

根據 Python 官方對 coroutine 定義:

Coroutines are a more generalized form of subroutines. Subroutines are entered at one point and exited at another point. Coroutines can be entered, exited, and resumed at many different points. They can be implemented with the async def statement.

簡單講 coroutine 可以在任意的時間點開始、暫停和離開,並且透過 async def 宣告此函數為一個 coroutine。

所以 await 的作用就是告訴 CPU 說可以暫停後面的工作,先去執行其他程式。另外 await 只能在 coroutine 中宣告,這就是為什麼 await 必須寫在 async def 裡面。

另一個要注意的點,await 後只能接 awaitables 物件,awaitables 物件就包括 coroutine, Task, Future 和有實作 __await__() 的物件。所以並不是所有函數都可以使用 await 加速。

coroutine 使用範例

最後來講 coroutine 的使用範例吧!

import asyncio

async def main():
    await asyncio.sleep(1)
    print('hello')

main()

執行後應該會出錯:

RuntimeWarning: coroutine 'main' was never awaited
  main()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

這是因為現在 main() 已經宣告成一個 coroutine 了,所以不能夠直接呼叫,而是要改成用 asyncio.run() 呼叫,所以將程式碼改成下面這樣就可以成功印出 hello 了。

import asyncio

async def main():
    await asyncio.sleep(1)
    print('hello')

asyncio.run(main())

好,async def & await 就大致介紹到這邊,關於 asyncio 有滿多東西可以玩的,有需要歡迎看這篇 Python asyncio 從不會到上路

FastAPI async def & await

最後回來看 FastAPI 文件 中怎麼說明 async def & await 的。

他說如果你需要在 path operation function 中呼叫一些很慢的函數 (如: 讀取資料庫、網路請求...) 時,就可以使用 async defawait 來加速,就像下面的例子。

@app.get('/')
async def read_results():
    results = await do_something()  # do_something() is slow
    return results

但如果不需要呼叫這些函數,就直接使用一般 def 即可。

現在就能明白為何 FastAPI 要使用 async defawait 了吧!


import asyncio
import multiprocessing as mp

async def crawl_data():
    while True:
        print('Crawling data...')
        await asyncio.sleep(1)

def start_crawler():
    asyncio.run(crawl_data())

if __name__ == '__main__':
    p = mp.Process(target=start_crawler)
    p.start()
    p.join()

EPS

import requests
import pandas as pd

pd.options.display.float_format = lambda x: "%.2f" % x


url = "https://api.finmindtrade.com/api/v4/data"
parameter = {
    "dataset": "TaiwanStockFinancialStatements",
    "data_id": "2330",
    "start_date": "2019-01-01",
    "token": "",  # 參考登入,獲取金鑰
}
data = requests.get(url, params=parameter)
data = data.json()
data = pd.DataFrame(data["data"])
eps_data = data[data["type"] == "EPS"]
eps_data.reset_index(drop=True, inplace=True)
print(eps_data) 
pip install FinMind

臺灣還原股價資料表 TaiwanStockPriceAdj

from FinMind.data import DataLoader

api = DataLoader()
# api.login_by_token(api_token='token')
# api.login(user_id='user_id',password='password')
df = api.taiwan_stock_daily_adj(
    stock_id="2330", start_date="2000-04-02", end_date="2023-04-12"
)
print(df)

股價日成交資訊 TaiwanStockPrice

from FinMind.data import DataLoader

api = DataLoader()
# api.login_by_token(api_token='token')
# api.login(user_id='user_id',password='password')
df = api.taiwan_stock_daily(
    stock_id='2330',
    start_date='2020-04-02',
    end_date='2020-04-12'
)

回測(引用外部 data)

import numpy as np
import pandas as pd
from FinMind import strategies
from FinMind.data import DataLoader
from FinMind.strategies.base import Strategy
from ta.momentum import StochasticOscillator


class ShortSaleMarginPurchaseRatio(Strategy):
    """
    summary:
        策略概念: 券資比越高代表散戶看空,法人買超股票會上漲,這時候賣可以跟大部分散戶進行相反的操作,反之亦然
        策略規則: 券資比>=30% 且法人買超股票, 賣
                券資比<30% 且法人賣超股票 買
    """

    ShortSaleMarginPurchaseTodayRatioThreshold = 0.3

    def load_taiwan_stock_margin_purchase_short_sale(self):
        self.TaiwanStockMarginPurchaseShortSale = self.data_loader.taiwan_stock_margin_purchase_short_sale(
            stock_id=self.stock_id, start_date=self.start_date, end_date=self.end_date,
        )
        self.TaiwanStockMarginPurchaseShortSale[
            ["ShortSaleTodayBalance", "MarginPurchaseTodayBalance"]
        ] = self.TaiwanStockMarginPurchaseShortSale[
            ["ShortSaleTodayBalance", "MarginPurchaseTodayBalance"]
        ].astype(
            int
        )
        self.TaiwanStockMarginPurchaseShortSale["ShortSaleMarginPurchaseTodayRatio"] = (
            self.TaiwanStockMarginPurchaseShortSale["ShortSaleTodayBalance"]
            / self.TaiwanStockMarginPurchaseShortSale["MarginPurchaseTodayBalance"]
        )

    def load_institutional_investors_buy_sell(self):
        self.InstitutionalInvestorsBuySell = self.data_loader.taiwan_stock_institutional_investors(
            stock_id=self.stock_id, start_date=self.start_date, end_date=self.end_date,
        )
        self.InstitutionalInvestorsBuySell[["sell", "buy"]] = (
            self.InstitutionalInvestorsBuySell[["sell", "buy"]].fillna(0).astype(int)
        )
        self.InstitutionalInvestorsBuySell = self.InstitutionalInvestorsBuySell.groupby(
            ["date", "stock_id"], as_index=False
        ).agg({"buy": np.sum, "sell": np.sum})
        self.InstitutionalInvestorsBuySell["diff"] = (
            self.InstitutionalInvestorsBuySell["buy"]
            - self.InstitutionalInvestorsBuySell["sell"]
        )

    def create_trade_sign(self, stock_price: pd.DataFrame) -> pd.DataFrame:
        stock_price = stock_price.sort_values("date")
        self.load_taiwan_stock_margin_purchase_short_sale()
        self.load_institutional_investors_buy_sell()
        stock_price = pd.merge(
            stock_price,
            self.InstitutionalInvestorsBuySell[["stock_id", "date", "diff"]],
            on=["stock_id", "date"],
            how="left",
        ).fillna(0)
        stock_price = pd.merge(
            stock_price,
            self.TaiwanStockMarginPurchaseShortSale[
                ["stock_id", "date", "ShortSaleMarginPurchaseTodayRatio"]
            ],
            on=["stock_id", "date"],
            how="left",
        ).fillna(0)
        stock_price.index = range(len(stock_price))
        stock_price["signal"] = 0
        sell_mask = (
            stock_price["ShortSaleMarginPurchaseTodayRatio"]
            >= self.ShortSaleMarginPurchaseTodayRatioThreshold
        ) & (stock_price["diff"] > 0)
        stock_price.loc[sell_mask, "signal"] = -1
        buy_mask = (
            stock_price["ShortSaleMarginPurchaseTodayRatio"]
            < self.ShortSaleMarginPurchaseTodayRatioThreshold
        ) & (stock_price["diff"] < 0)
        stock_price.loc[buy_mask, "signal"] = 1
        return stock_price


data_loader = DataLoader()
# data_loader.login(user_id, password) # 可選
obj = strategies.BackTest(
    stock_id="0056",
    start_date="2018-01-01",
    end_date="2019-01-01",
    trader_fund=500000.0,
    fee=0.001425,
    data_loader=data_loader,
)

obj.add_strategy(ShortSaleMarginPurchaseRatio)
obj.simulate()

print(obj.final_stats)
print(obj.trade_detail)
obj.plot()

Python 即時資料 Pipeline,以版塊圖X即時股市資料為例

https://medium.com/finmind/python-%E5%8D%B3%E6%99%82%E8%B3%87%E6%96%99-pipeline-%E4%BB%A5%E7%89%88%E5%A1%8A%E5%9C%96x%E5%8D%B3%E6%99%82%E8%82%A1%E5%B8%82%E8%B3%87%E6%96%99%E7%82%BA%E4%BE%8B-a55de908dd5b

import os
import typing

import numpy as np
import pandas as pd
import plotly.express as px
import requests
from apscheduler.schedulers.background import BackgroundScheduler
from FinMind.data import DataLoader
from flask import Flask
from loguru import logger


class TreeMap:
    def __init__(self):
        self.token = os.environ.get("FINMIND_API_TOKEN")
        self.html = "初始化~~~"
        self.api = DataLoader()
        self.api.login_by_token(api_token=self.token)
        self.stock_info = self.api.taiwan_stock_info()
        self.data_clean()

    def data_clean(self) -> typing.Tuple[pd.DataFrame, pd.DataFrame]:
        logger.info("data_clean")
        self.stock_info.drop(["date", "type"], axis=1, inplace=True)

    def filter_top_5_stock(self, plot_df: pd.DataFrame) -> pd.DataFrame:
        top_df = plot_df[["stock_id", "industry_category", "Trading_Money"]]
        top_df = top_df.sort_values("Trading_Money", ascending=False)
        top_df = top_df.groupby("industry_category").head(5)
        top_df = top_df[["stock_id", "industry_category"]]
        plot_df = top_df.merge(
            plot_df, how="left", on=["stock_id", "industry_category"]
        )
        return plot_df

    def feature_engineer(self, snapshot_df: pd.DataFrame):
        logger.info("feature_engineer")
        last_datetime = max(snapshot_df["date"])
        plot_df = snapshot_df[
            ["stock_id", "total_amount", "change_rate", "close"]
        ]
        plot_df.columns = ["stock_id", "Trading_Money", "漲跌幅%", "close"]
        plot_df = plot_df.merge(self.stock_info, how="inner", on=["stock_id"])
        for col in ["Index", "大盤"]:
            plot_df = plot_df[plot_df["industry_category"] != col]

        index_df = plot_df.groupby(["industry_category"])["Trading_Money"].agg(
            sum
        )
        index_df = index_df.reset_index()
        index_df.columns = ["industry_category", "Index_Trading_Money"]
        plot_df = plot_df.merge(index_df, how="inner", on=["industry_category"])
        plot_df = self.filter_top_5_stock(plot_df)
        plot_df["stock_name"] = (
            plot_df["stock_id"] + " " + plot_df["stock_name"]
        )
        plot_df["spread_rate_label"] = plot_df["漲跌幅%"].astype(str)
        return plot_df, last_datetime

    def plot(self, plot_df: pd.DataFrame, last_datetime: str):
        logger.info("plot")
        fig = px.treemap(
            plot_df,
            path=["industry_category", "stock_name"],
            values="Trading_Money",
            color="漲跌幅%",
            color_continuous_scale=[[0, "green"], [0.5, "white"], [1, "red"]],
            color_continuous_midpoint=0,
            custom_data=["stock_name", "close", "spread_rate_label"],
            title=f"臺股交易額X漲跌幅 {last_datetime}",
            width=1350,
            height=900,
        )
        texttemplate = "%{customdata[0]}<br>收盤價 %{customdata[1]}<br>漲跌幅(%) %{customdata[2]}<br>"
        fig.update_traces(
            textposition="middle center",
            textfont_size=24,
            texttemplate=texttemplate,
        )
        # fig.data[0].labels
        fig.data[0]["marker"]["colors"] = np.round(
            fig.data[0]["marker"]["colors"], 2
        )

        html = fig.to_html()
        return html

    def get_snapshot(self) -> pd.DataFrame:
        logger.info("get snapshot")
        url = "https://api.finmindtrade.com/api/v4/taiwan_stock_tick_snapshot"
        parameter = {
            "token": self.token,  # 參考登入,獲取金鑰
        }
        resp = requests.get(url, params=parameter)
        data = resp.json()
        if data["status"] != 200:
            raise Exception(data["msg"])
        df = pd.DataFrame(data["data"])
        return df

    def main(self):
        # load data
        snapshot_df = self.get_snapshot()
        # feature engineer
        plot_df, last_datetime = self.feature_engineer(snapshot_df)
        # plot
        self.html = self.plot(plot_df, last_datetime)


def set_scheduler():
    scheduler = BackgroundScheduler(
        timezone="Asia/Taipei", job_defaults={"max_instances": 1}
    )
    scheduler.add_job(
        id="snapshot",
        func=tree_map.main,
        trigger="cron",
        day_of_week="*",
        hour="*",
        minute="*",
        second="*/5",
    )
    scheduler.start()
    logger.info("scheduler start")


app = Flask(__name__)
tree_map = TreeMap()


@app.route("/", methods=["GET", "POST"])
def submit():
    html = tree_map.html
    return f"""
        <meta http-equiv="refresh" content="1" /> 
        {html}
    """


set_scheduler()
app.run(host="0.0.0.0", debug=True)

Python Telegram Bot

https://hackmd.io/@truckski/HkgaMUc24?type=view

@BotFather 申請一個 Bot。

  1. /newbot
  2. 輸入名稱
  3. 輸入 username
  4. 記下 token

New Bot

hello, world

執行這個程式,注意 'YOUR TOKEN HERE' 的地方請填入前面得到的 Token。

from telegram.ext import Updater, CommandHandler

def hello(bot, update):
    update.message.reply_text(
        'hello, {}'.format(update.message.from_user.first_name))


updater = Updater('YOUR TOKEN HERE')

updater.dispatcher.add_handler(CommandHandler('hello', hello))

updater.start_polling()
updater.idle()

用 Bot 的 username 或是 BotFather 給的連結可以找到前面建立的 Bot。 對它輸入 /hello

hello

Command Handler 可從 update 獲得的資訊

  • update
    • update_id
    • message
      • message_id
      • from_user:發訊人
        • id
        • first_name
        • last_name
        • full_name
        • username
      • chat:訊息所在的聊天室
        • id
        • type
      • text:訊息內容

更多資訊

傳訊息

  • bot.send_message(chat_id, text)
  • update.message.reply_text(text):Shortcut for bot.send_message(update.message.chat_id, text)

更多功能

範例 - 語錄 Bot

import random, os
from telegram.ext import Updater, CommandHandler

# 把語錄檔案載入
if os.path.exists('sentences.txt'):
    with open('sentences.txt') as FILE:
        sentences = [sentence.strip() for sentence in FILE]
else:
    sentences = []

def add(bot, update):
    print('from user:', update.message.from_user.id)
    # 限制只有特定人才能新增語錄
    # if update.message.from_user.id == YOUR_USER_ID_HERE:
    if True:
        sentence = update.message.text[5:].replace('\n', ' ')
        sentences.append(sentence)
        with open('sentences.txt', 'a') as FILE:
            print(sentence, file=FILE)
        update.message.reply_text('已加入:' + sentence)

def say(bot, update):
    if sentences:
        update.message.reply_text(random.choice(sentences))
    else:
        update.message.reply_text('I have no words.')

updater = Updater('YOUR TOKEN HERE')

updater.dispatcher.add_handler(CommandHandler('add', add))
updater.dispatcher.add_handler(CommandHandler('say', say))

updater.start_polling()
updater.idle()

語錄

互動按鈕

send_message 加上 reply_markup = InlineKeyboardMarkup(...) 就會在該訊息附上按鈕。

from telegram.ext import Updater, CommandHandler
from telegram import InlineKeyboardMarkup, InlineKeyboardButton

def start:
    bot.send_message(chat_id, '參考資料',
        reply_markup = InlineKeyboardMarkup([[
            InlineKeyboardButton('課程網站', url = 'https://github.com/mzshieh/pa19spring'),
            InlineKeyboardButton('Documentation', url = 'https://python-telegram-bot.readthedocs.io/en/stable/index.html')]]))

# ...

img

除了 url 以外,也可以用 callback_data 來讓 Bot 知道哪個按鈕被按了。

from random import randint
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler
from telegram import InlineKeyboardMarkup, InlineKeyboardButton

def start(bot, update):
    a, b = randint(1, 100), randint(1, 100)
    update.message.reply_text('{} + {} = ?'.format(a, b),
        reply_markup = InlineKeyboardMarkup([[
                InlineKeyboardButton(str(s), callback_data = '{} {} {}'.format(a, b, s)) for s in range(a + b - randint(1, 3), a + b + randint(1, 3))
            ]]))

def answer(bot, update):
    a, b, s = [int(x) for x in update.callback_query.data.split()]
    if a + b == s:
        update.callback_query.edit_message_text('你答對了!')
    else:
        update.callback_query.edit_message_text('你答錯囉!')

updater = Updater('YOUR TOKEN HERE')

updater.dispatcher.add_handler(CommandHandler('start', start))
updater.dispatcher.add_handler(CallbackQueryHandler(answer))

updater.start_polling()
updater.idle()

70 + 90 = ?

你答對了!

Callback Query Handler 可從 update 獲得的資訊

  • update
    • update_id
    • callback_query
      • from_user
      • message:按鈕依附的 message
      • data:建立 InlineKeyboardButton 時傳入的 callback_data

更多資訊

回應 Callback Query

  • bot
    • answer_callback_query(callback_query_id, text):會顯示文字在畫面中間。
    • edit_message_text(chat_id = string, message_id = string, text):修改文字,會同時清除按鈕。
  • update.callback_query
    • answer(text):Shortcut for bot.answer_callback_query(update.callback_query.id, text)
    • edit_message_text(text):Shortcut for bot.edit_message_text(chat_id=update.callback_query.message.chat_id, message_id=update.callback_query.message.message_id, text

更多功能

範例 - 剪刀石頭布

import random
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler
from telegram import InlineKeyboardMarkup, InlineKeyboardButton

hands = ['rock', 'paper', 'scissors']

emoji = {
    'rock': '👊',
    'paper': '✋',
    'scissors': '✌️'
} 

def start(bot, update):
    update.message.reply_text('剪刀石頭布!',
        reply_markup = InlineKeyboardMarkup([[
                InlineKeyboardButton(emoji, callback_data = hand) for hand, emoji in emoji.items()
            ]]))

def judge(mine, yours):
    if mine == yours:
        return '平手'
    elif (hands.index(mine) - hands.index(yours)) % 3 == 1:
        return '我贏了'
    else:
        return '我輸了'

def play(bot, update):
    try:
        mine = random.choice(hands)
        yours = update.callback_query.data
        update.callback_query.edit_message_text('我出{},你出{},{}!'.format(emoji[mine], emoji[yours], judge(mine, yours)))
    except Exception as e:
        print(e)

updater = Updater('YOUR TOKEN HERE')

updater.dispatcher.add_handler(CommandHandler('start', start))
updater.dispatcher.add_handler(CallbackQueryHandler(play))

updater.start_polling()
updater.idle()

剪刀石頭布! 我輸了!

Reference

https://python-telegram-bot.readthedocs.io/en/stable/index.html


使用Python寫一個Telegram Notify

第一步: 建立你的Bot名稱與代號

img

在 Telegram 世界中,管理Bot的叫做 BotFather,請認明第一位有藍勾勾的官方Bot。不要選到下面奇怪的 Bot Fater了XD

跟他對話之後,基本上跟他說聲Hi,他就會吐出

"I can help you create and manage Telegram bots. If you’re new to the Bot API, please see the manual… "

接下來,請輸入 */newbot* 以創建新的Bot。接著幫她建立名稱跟她專屬的ID。

img

黑色的部分為你這個Bot的Token,類似金鑰

第二步: 建立群組,並將你的Bot加入群組

不過Telegram在創建群組上,至少要先加入一個真人,所以你可以加一位好友進去,然後再加入你剛剛創建的Bot,最後再跟你朋友說聲Goodbye把她Remove掉。

第三步: 取得你的ChatID

前往網址 https://api.telegram.org/bot{your bot token}/getUpdates

而{your bot token} 就是填入Bot Father賦予你的Bot Token(不用加括號)

如果成功前往,應該會看到像是 *{“ok”:true,”result”:[]}* 的資訊。 這時候,前往你的群組輸入 */my_id @你的BotID*,接著再次重新整理網頁。就會看到以下的資訊。

img

橘色部分為你的Chat ID

好了,現在有Token,也有ChatID,終於可以開始進入程式部分

第四步: 用 Python發送Message

接者使用request.get的方式,去API上通知BOT發送訊息。 而因為可能每次Bot要打出去的訊息不同,所以將這個功能模組化,以下為範例參考。

img

結語

以上就是利用簡單的範例來瞭解如何創建一個簡易的Telegram Notify。 應用範圍可以用來推播日常工作排程上的Error Bug或搭配Timer傳送你寫的爬蟲資料,是不是非常好用呢! (而且必要的程式碼也是非常少呢)

如果還有興趣開發功能更強大的Bot,我把連結放在下方,大家就參考看看囉!


import os
import telebot
import requests
from loguru import logger


TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
bot = telebot.TeleBot(TELEGRAM_BOT_TOKEN)


def get_daily_horoscope(sign: str, day: str) -> dict:
    """通過特定的星座獲取運勢。

    關鍵字解釋:
    sign:str - 星座
    day:str - 格式化的日期 (YYYY-MM-DD) 或 TODAY 或 TOMORROW 或 YESTERDAY
    Return:dict - JSON data
    """
    url = "https://horoscope-app-api.vercel.app/api/v1/get-horoscope/daily"
    params = {"sign": sign, "day": day}
    response = requests.get(url, params)

    return response.json()


@bot.message_handler(commands=["start", "hello"])
def send_welcome(message):
    bot.reply_to(message, "Howdy, how are you doing?")


@bot.message_handler(commands=["horoscope"])
def sign_handler(message):
    text = "What's your zodiac sign?\nChoose one: *Aries*, *Taurus*, *Gemini*, *Cancer,* *Leo*, *Virgo*, *Libra*, *Scorpio*, *Sagittarius*, *Capricorn*, *Aquarius*, and *Pisces*."
    sent_msg = bot.send_message(message.chat.id, text, parse_mode="Markdown")
    bot.register_next_step_handler(sent_msg, day_handler)


def day_handler(message):
    sign = message.text
    text = "What day do you want to know?\nChoose one: *TODAY*, *TOMORROW*, *YESTERDAY*, or a date in format YYYY-MM-DD."
    sent_msg = bot.send_message(message.chat.id, text, parse_mode="Markdown")
    bot.register_next_step_handler(sent_msg, fetch_horoscope, sign.capitalize())


def fetch_horoscope(message, sign):
    day = message.text
    horoscope = get_daily_horoscope(sign, day)
    data = horoscope["data"]
    horoscope_message = (
        f'*Horoscope:* {data["horoscope_data"]}\n*Sign:* {sign}\n*Day:* {data["date"]}'
    )
    bot.send_message(message.chat.id, "Here's your horoscope!")
    bot.send_message(message.chat.id, horoscope_message, parse_mode="Markdown")


@bot.message_handler(func=lambda msg: True)
def echo_all(message):
    print(message, message.text)
    bot.reply_to(message, message.text)


try:
    bot.infinity_polling()
except Exception as e:
    logger.exception(e)

Python 中殺死執行緒的幾個方法

通常,突然終止執行緒被認為是一種糟糕的程式設計習慣。突然終止執行緒可能會使必須正確關閉的關鍵資源處於打開狀態。但是您可能希望在某個特定時間段過去或產生某個中斷後終止執行緒。

下面有6種方式殺死執行緒,其中前兩種較常用,我在原文的基礎上進行了一些修改,以覆蓋更多的情況。

  • 在 python 執行緒中拋出異常
  • 使用 flag / Event()
  • 使用 traces 殺死執行緒
  • 使用 multiprocessing module 殺死執行緒
  • 通過將其設定為守護處理程序來殺死 Python 執行緒
  • 使用隱藏函數 _stop()

0x01 在 python 執行緒中拋出異常

此方法使用函數 PyThreadState_SetAsyncExc() 線上程中引發異常。

這種方法主要分兩步。第一步獲取執行緒id,第二步呼叫 SetAsyncExc,另外可以通過返回值查看是否殺死成功。 例如:

# Python program raising
# exceptions in a python
# thread

import threading
import ctypes
import time

class thread_with_exception(threading.Thread):
	def __init__(self, name):
		threading.Thread.__init__(self)
		self.name = name
			
	def run(self):

		# target function of the thread class
		try:
			while True:
				print('running ' + self.name)
		finally:
			print('ended')
		
	def get_id(self):

		# returns id of the respective thread
		if hasattr(self, '_thread_id'):
			return self._thread_id
		for id, thread in threading._active.items():
			if thread is self:
				return id

	def raise_exception(self):
		thread_id = self.get_id()
		res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id,
			ctypes.py_object(SystemExit))
		if res > 1:
			ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0)
			print('Exception raise failure')
	
t1 = thread_with_exception('Thread 1')
t1.start()
time.sleep(2)
t1.raise_exception()
t1.join()

另外,這裡還有另一種方法獲取執行緒的 id:

import threading
import time
import inspect
import ctypes


def _async_raise(tid, exctype):
    """Raises an exception in the threads with id tid"""
    if not inspect.isclass(exctype):
        raise TypeError("Only types can be raised (not instances)")
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), ctypes.py_object(exctype))
    if res == 0:
        raise ValueError("invalid thread id")
    elif res != 1:
        # """if it returns a number greater than one, you're in trouble,
        # and you should call it again with exc=NULL to revert the effect"""
        ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
        raise SystemError("PyThreadState_SetAsyncExc failed")


def stop_thread(thread):
    _async_raise(thread.ident, SystemExit)


class TestThread(threading.Thread):
    def run(self):
        print("begin run the child thread")
        while True:
            print("sleep 1s")
            time.sleep(1)


if __name__ == "__main__":
    print("begin run main thread")
    t = TestThread()
    t.start()
    time.sleep(3)
    stop_thread(t)
    print("main thread end")

當我們在一臺機器上運行上面的程式碼時,你會注意到,只要函數 raise_exception() 被呼叫,目標函數 run() 就會結束。這是因為一旦引發異常,程序控制就會跳出 try 塊,run() 函數將終止。之後可以呼叫 join() 函數來終止執行緒。在沒有函數 run_exception() 的情況下,目標函數 run() 將一直運行,並且永遠不會呼叫 join() 函數來終止執行緒。

0x02 使用 flag

為了殺死一個執行緒,我們可以聲明一個停止標誌,這個標誌會被執行緒檢查。例如

# Python program showing
# how to kill threads
# using set/reset stop
# flag

import threading
import time

def run():
	while True:
		print('thread running')
		global stop_threads
		if stop_threads:
			break

stop_threads = False
t1 = threading.Thread(target = run)
t1.start()
time.sleep(1)
stop_threads = True
t1.join()
print('thread killed')

在上面的程式碼中,一旦設定了全域變數 stop_threads,目標函數 run() 就會結束,並且可以使用 t1.join() 殺死執行緒 t1。但是由於某些原因,人們可能會避免使用全域變數。對於這些情況,可以傳遞函數對象以提供類似的功能,如下所示。

# Python program killing
# threads using stop
# flag

import threading
import time

def run(stop):
	while True:
		print('thread running')
		if stop():
				break
				
def main():
		stop_threads = False
		t1 = threading.Thread(target = run, args =(lambda : stop_threads, ))
		t1.start()
		time.sleep(1)
		stop_threads = True
		t1.join()
		print('thread killed')
main()

上面程式碼中傳入的函數對象總是返回局部變數 stop_threads 的值。這個值在函數 run() 中被檢查,一旦 stop_threads 被重設,run() 函數結束並且執行緒可以被殺死。

另外,使用 threading.Event() 可以更優雅的實現這一功能。

0x03 使用 traces 殺死執行緒

此方法通過在每個執行緒中使用 traces 來工作。每個 trace 都會在檢測到某些刺激或標誌時自行終止,從而立即終止關聯的執行緒。例如

Python program using
# traces to kill threads

import sys
import trace
import threading
import time
class thread_with_trace(threading.Thread):
def __init__(self, *args, **keywords):
	threading.Thread.__init__(self, *args, **keywords)
	self.killed = False

def start(self):
	self.__run_backup = self.run
	self.run = self.__run	
	threading.Thread.start(self)

def __run(self):
	sys.settrace(self.globaltrace)
	self.__run_backup()
	self.run = self.__run_backup

def globaltrace(self, frame, event, arg):
	if event == 'call':
	return self.localtrace
	else:
	return None

def localtrace(self, frame, event, arg):
	if self.killed:
	if event == 'line':
		raise SystemExit()
	return self.localtrace

def kill(self):
	self.killed = True

def func():
while True:
	print('thread running')

t1 = thread_with_trace(target = func)
t1.start()
time.sleep(2)
t1.kill()
t1.join()
if not t1.isAlive():
print('thread killed')

在這段程式碼中,start() 被稍微修改為使用 settrace() 設定系統跟蹤功能。本地跟蹤函數的定義是,無論何時設定相應執行緒的終止標誌(已終止),都會引發 SystemExit 異常執行下一行程式碼,結束目標函數func的執行。現在可以使用 join() 終止執行緒。

0x04 使用 multiprocessing module 殺死執行緒

Python 的multiprocessing module 允許您以類似於使用執行緒模組生成執行緒的方式生成處理程序。multiprocessing module 的介面與 threading 的介面類似。例如,在給定的程式碼中,我們建立了三個執行緒(處理程序),它們從 1 計數到 9。

# Python program creating
# three threads

import threading
import time

# counts from 1 to 9
def func(number):
	for i in range(1, 10):
		time.sleep(0.01)
		print('Thread ' + str(number) + ': prints ' + str(number*i))

# creates 3 threads
for i in range(0, 3):
	thread = threading.Thread(target=func, args=(i,))
	thread.start()

上述程式碼的功能也可以通過類似的方式使用多處理模組來實現,只需很少的改動。請參閱下面給出的程式碼。

# Python program creating
# thread using multiprocessing
# module

import multiprocessing
import time

def func(number):
	for i in range(1, 10):
		time.sleep(0.01)
		print('Processing ' + str(number) + ': prints ' + str(number*i))

for i in range(0, 3):
	process = multiprocessing.Process(target=func, args=(i,))
	process.start()

儘管這兩個模組的介面相似,但是這兩個模組的實現卻截然不同。所有執行緒共享全域變數,而處理程序彼此完全分離。因此,與殺死執行緒相比,殺死處理程序要安全得多。 Process 類提供了一種方法 terminate() 來終止處理程序。現在,回到最初的問題。假設在上面的程式碼中,我們想要在 0.03s 過去後殺死所有處理程序。此功能是使用以下程式碼中的 multiprocessing 實現的。

# Python program killing
# a thread using multiprocessing
# module

import multiprocessing
import time

def func(number):
	for i in range(1, 10):
		time.sleep(0.01)
		print('Processing ' + str(number) + ': prints ' + str(number*i))

# list of all processes, so that they can be killed afterwards
all_processes = []

for i in range(0, 3):
	process = multiprocessing.Process(target=func, args=(i,))
	process.start()
	all_processes.append(process)

# kill all processes after 0.03s
time.sleep(0.03)
for process in all_processes:
	process.terminate()

雖然這兩個模組有不同的實現。上面程式碼中多處理模組提供的這個功能類似於殺死執行緒。因此,只要我們需要在 Python 中實現執行緒終止,multiprocessing 就可以作為一個簡單的替代方案。

0x05 通過將其設定為守護處理程序(daemon)來殺死 Python 執行緒

守護執行緒 是那些在主程序退出時被殺死的執行緒。例如

import threading
import time
import sys

def func():
	while True:
		time.sleep(0.5)
		print("Thread alive, and it won't die on program termination")

t1 = threading.Thread(target=func)
t1.start()
time.sleep(2)
sys.exit()

請注意,執行緒 t1 保持活動狀態並阻止主程序通過 sys.exit() 退出。在 Python 中,任何活動的非守護執行緒都會阻止主程序退出。然而,一旦主程序退出,守護執行緒本身就會被殺死。換句話說,主程序一退出,所有的守護執行緒就被殺死了。要將執行緒聲明為守護處理程序,我們將關鍵字參數 daemon 設定為 True。例如,在給定的程式碼中,它演示了守護執行緒的屬性。

# Python program killing
# thread using daemon

import threading
import time
import sys

def func():
	while True:
		time.sleep(0.5)
		print('Thread alive, but it will die on program termination')

t1 = threading.Thread(target=func)
t1.daemon = True
t1.start()
time.sleep(2)
sys.exit()

請注意,一旦主程序退出,執行緒 t1 就會被終止。在程序終止可用於觸發執行緒終止的情況下,此方法被證明非常有用。請注意,在 Python 中,只要所有非守護執行緒都死了,主程序就會終止,而不管有多少守護執行緒處於活動狀態。因此,這些守護執行緒所持有的資源,例如打開的檔案、資料庫事務等,可能無法正常釋放。 python 程序中的初始控制執行緒不是守護執行緒。除非確定知道這樣做不會導致任何洩漏或死鎖,否則不建議強行終止執行緒。

0x06 使用隱藏函數 _stop()

為了殺死一個執行緒,我們使用隱藏函數 _stop() 這個函數沒有記錄但可能會在下一版本的 python 中消失。

# Python program killing
# a thread using ._stop()
# function

import time
import threading

class MyThread(threading.Thread):

	# Thread class with a _stop() method.
	# The thread itself has to check
	# regularly for the stopped() condition.

	def __init__(self, *args, **kwargs):
		super(MyThread, self).__init__(*args, **kwargs)
		self._stop = threading.Event()

	# function using _stop function
	def stop(self):
		self._stop.set()

	def stopped(self):
		return self._stop.isSet()

	def run(self):
		while True:
			if self.stopped():
				return
			print("Hello, world!")
			time.sleep(1)

t1 = MyThread()

t1.start()
time.sleep(5)
t1.stop()
t1.join()

注意: 以上方法在某些情況下可能不起作用,因為 python 沒有提供任何直接殺死執行緒的方法。

Python之Websocket介紹與實作

這幾天學了一些Websocket的知識,發現網路上中文文檔非常少,就來分享一下學習心得

  • Websocket是應用層協議,服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發送信息,是真正的雙向平等對話(全雙工)
  • http是client去request server,然後server return response,HTTP通信只能由客戶端發起(輪巡) 輪詢的效率低,非常浪費資源(因為必須不停連接,或者HTTP 連接始終打開)

img

  • 是應用層的協議,建立在TCP協議上,握手時採用HTTP,只要握手一次就好
  • 和AJAX不同,AJAX是為了達到"推送"技術 而不斷的輪巡,這種傳統的模式帶來很明顯的缺點,即瀏覽器需要不斷的向服務器發出請求,然而HTTP請求可能包含較長的header,其中真正有效的數據可能只是很小的一部分,顯然這樣會浪費很多的帶寬等資源。
  • HTML5 定義的WebSocket 協議,數據格式比較輕量,開銷小,能更好的節省服務器資源和帶寬,並且能夠更實時地進行通訊。
  • 關於AJAX和Websocket的比較 可以看這篇

接下來就是實作了,python要實作websocket的話,可以使用websockets 這個函式庫,這個library比較多人用,文檔也很詳細容易上手,不過django和flask應該都有對應的lib,這大家需要再研究吧

備註:接下來會用到異步asyncio的概念,若不熟的可能要去看我寫的這篇 或者real-python上也寫得很好,主要大概就是async def, await, coroutine這些用法要熟就可以實作websocket了

記得先pip install websockets (然後python版本我用的是3.7,至少需大於3.5,否則不支持asyncio)

client.py

import asyncio
import websockets

async def hello(uri):
    async with websockets.connect(uri) as websocket:
        await websocket.send("Jimmy")
        print(f"(client) send to server: Jimmy")
        name = await websocket.recv()
        print(f"(client) recv from server {name}")

asyncio.get_event_loop().run_until_complete(
    hello('ws://localhost:8765'))

server.py (先打開這個,再打開client.py)

import asyncio
import websockets

async def echo(websocket, path):
    print('echo')
    async for message in websocket:
        print(message,'received from client')
        greeting = f"Hello {message}!"
        await websocket.send(greeting)
        print(f"> {greeting}")

asyncio.get_event_loop().run_until_complete(
    websockets.serve(echo, 'localhost', 8765))
asyncio.get_event_loop().run_forever()

整個流程大概是一開始server用websockets.serve註冊一個websocket sever,並將handler指定給echo,(echo這個function--> It must be a coroutine accepting two arguments: a **WebSocketServerProtocol** and the request URI.) 直接看api文檔可以瞭解參數為何https://websockets.readthedocs.io/en/stable/api.html client這邊一打開就會連至ws,然後發送字串,server收到後就會echo回去,client收完後,就會關閉client

img

實作html5(client)與python(server)之多人記數器

實作code就在這裡 ,裡面的Synchronization example 我稍微解說一下

start_server = websockets.serve(counter, “localhost”, 6789)註冊了counter當作handler,所以每次進來的訊息都會到counter Global中用了USERS( set 用來計算多少用戶同時在線上) 和STATE(計算計數) 每次進來都會register(websocket),而try的finally就會unregister(websocket) (這邊就是用來統計使用者個數,當使用者個數產生變動,就會notify_users,[user.send(message) for user in USERS]的方式廣播變動

而計數也是一樣,操作加減js控件時,會透過ws去send json format async for message in websocket: data = json.loads(message) 這段就是負責接收加減數字的訊號

簡單來說 counter當作handler,連線進來時增加使用者個數,並監控使用者前端操作js發出來的websocket訊息(json),而這些異動都存在global的 USERS和STATE,異動後我就會根據目前使用者數量去notify_state & notify_users 這段就是websocket的重點,Server主動推送訊息

img

更進一步:基於websocket的聊天室

接下來有興趣的就可以去實作聊天室了(下面連結提供大陸網友實作的code) python websockets 網絡聊天室V0

解說一下,其實後端的邏輯十分簡單,會根據chat 這個main function去分不同的訊息處理(switch case 像是有人發訊息,有人login,logout,然後再把msg群發給所有User) 而前端接收到訊息後再呈現

其他像是Flask等等也有內建flask-sockets的函式庫,有興趣的再自己研究吧 https://github.com/heroku-examples/python-websockets-chat


連續發送 websocket_server.py

import asyncio
import websockets

async def echo(websocket, path):
    print('echo')
    i = 0
    while True:
        i += 1
        greeting = f"Hello {i}!"
        await websocket.send(greeting)
        print(f"> {greeting}")
        await asyncio.sleep(1)

asyncio.get_event_loop().run_until_complete(
    websockets.serve(echo, 'localhost', 8765))
asyncio.get_event_loop().run_forever()

websocket_client.py 使用 websocket.WebSocketApp

import threading
import time
import websocket
import queue
import rel


class ClientSocket:
    def __init__(self):
        websocket.enableTrace(True)
        self.ws = websocket.WebSocketApp(
            "ws://localhost:8765/",
            # "wss://api.gemini.com/v1/marketdata/BTCUSD",
            on_open=self.on_open,
            on_message=self.on_message,
            on_error=self.on_error,
            on_close=self.on_close,
        )
        # ping_interval 表示發送心跳訊息的間隔,預設為 0,表示不發送心跳訊息;
        # 而 ping_timeout 表示伺服器端的回應時間限制,預設為 20 秒。
        # 如果伺服器端在 ping_timeout 時間內沒有回應心跳訊息,則客戶端會認為伺服器端已經斷線,並關閉 WebSocket 連線。
        self.ws.run_forever(
            dispatcher=rel,
            ping_interval=10,  # 發送心跳訊息的間隔為 10 秒
            ping_timeout=5,  # 設定伺服器端的回應時間限制為 5 秒
        )
        rel.signal(2, rel.abort)  # Keyboard Interrupt
        rel.dispatch()

    def on_message(self, ws, message):
        print(message)

    def on_error(self, ws, error):
        print(error)

    def on_close(self, ws, close_status_code, close_msg):
        print("### closed ###")
        rel.abort()
        self.ws.run_forever(dispatcher=rel)
        rel.signal(2, rel.abort)
        rel.dispatch()

    def on_open(self, ws):
        print("Opened connection")


if __name__ == "__main__":
    client_socket = ClientSocket()

從零開始使用 Poetry

出處: https://blog.kyomind.tw/python-poetry/

本文所有的參考資料會放在文末的「參考」一欄中,不過在此還是要特別提及主要的參考對象,總共有二:

如果在本文找不到你需要的內容,以上二處可能會有,所以主動列出。

另外本文主要以 macOS 和 Linux(Ubuntu)環境來進行安裝及教學,Windows 用戶如果有無法順利安裝的情況,建議參考官方文件內容修正。不過,即使有問題,應該也是集中在安裝與設定階段,本文其餘部分仍可適用。

安裝 Poetry

Poetry 和 pip、git、pyenv 等工具一樣,都是典型的命令列工具,需要先安裝才能下達指令——poetry

安裝方式選擇

Poetry 主要提供了兩種安裝方式

  1. 全域安裝至使用者的家目錄。
  2. pip 安裝至專案使用的 Python(虛擬)環境,即pip install poetry

個人推薦使用全域安裝,官方文件也表示不推薦使用 pip 安裝

因為 pip 安裝是直接安裝到「專案所屬的 Python 虛擬環境」裡,而 Poetry 所依賴的套件非常多,總計超過 30 個,會嚴重影響專案虛擬環境的整潔度。文件中也警告這些依賴套件可能和專案本身的套件發生衝突

Be aware that it will also install Poetry’s dependencies which might cause conflicts with other packages.

全域安裝 Poetry 至家目錄

所以我們就使用全域安裝吧!

macOS / Linux / WSL(Windows Subsystem for Linux)

curl -sSL https://install.python-poetry.org | python3 -

curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -

Windows

(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -

Poetry 實際安裝路徑如下:

The installer installs the poetry tool to Poetry’s bin directory. This location depends on your system:

  • $HOME/.local/bin for Unix
  • %APPDATA%\Python\Scripts on Windows

以 macOS 為例,如果要下poetry指令,就需要打完整路徑$HOME/.local/bin/poetry,顯然不太方便,所以我們需要設定 PATH。

設定 PATH

新增poetry指令執行檔所在的路徑至 PATH。

.zshrc.bashrc.bash_profile新增:

export PATH=$PATH:$HOME/.local/bin

存檔後重啟 shell 即可使用。直接在命令列打上poetry指令測試:

❯ poetry
Poetry version 1.1.13

USAGE
  poetry [-h] [-q] [-v [<...>]] [-V] [--ansi] [--no-ansi] [-n] <command> [<arg1>] ... [<argN>]

...

設定 alias

比起pippoetry這個指令實在太冗長了!我們還是給它一個 alias 吧!

基於它是我極為常用的指令,我願意賦與它**「單字母」的 alias 特權**,我使用p

alias p='poetry'

測試結果:

❯ p
Poetry version 1.1.13

USAGE
  poetry [-h] [-q] [-v [<...>]] [-V] [--ansi] [--no-ansi] [-n] <command> [<arg1>] ... [<argN>]

alias 是方便自己使用,但本文基於表達清晰考量,下面的解說除了圖片外,原則上並不會使用 alias 表示。


初始化 Poetry 專案

為了方便解說,我們先建立一個全新的專案,名為poetry-demo

指令都很簡單,但還是建議可以一步一步跟著操作。

就像 git 專案需要初始化,Poetry 也需要,因為每一個使用了 Poetry 的專案中一定要有一個pyproject.toml作為它的設定檔。否則直接使用poetry相關指令就會出現下列錯誤訊息:

Poetry could not find a pyproject.toml file in {cwd} or its parents

所以一定先初始化,使用poetry init

mkdir poetry-demo
cd poetry-demo
poetry init

此時會跳出一連串的互動對話,協助你建立專案的資料,大部分可以直接enter跳過:

This command will guide you through creating your pyproject.toml config.

Package name [poetry-demo]:
Version [0.1.0]:
Description []:
Author [kyo <odinxp@gmail.com>, n to skip]:
License []:
Compatible Python versions [^3.8]:

Would you like to define your main dependencies interactively? (yes/no) [yes]

直到出現「Would you like to define your main dependencies interactively? (yes/no) [yes]」,我們先選擇「no」後,會讓你確認本次產生的toml檔內容:

Would you like to define your development dependencies interactively? (yes/no) [yes] no
Generated file

[tool.poetry]
name = "poetry-demo"
version = "0.1.0"
description = ""
authors = ["kyo <odinxp@gmail.com>"]

[tool.poetry.dependencies]
python = "^3.8"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

並詢問你「Do you confirm generation? (yes/no) [yes]」,按enter使用預設選項「yes」或直接回答「yes」,則pyproject.toml建立完成。

此時專案目錄結構如下:

poetry-demo
└── pyproject.toml

0 directories, 1 file

管理 Poetry 虛擬環境

我覺得學習 Poetry 的第一道關卡,就是它對於虛擬環境的管理。

「強制」虛擬環境

Poetry 預設上(可透過poetry config修改)會強制套件都要安裝在虛擬環境中,以免汙染全域,所以它整合了virtualenv

所以在執行poetry add、install等指令時,Poetry 都會自動檢查當下是否正在使用虛擬環境:

  • 如果,則會直接安裝套件至當前的虛擬環境。
  • 如果,則會自動幫你建立一個新的虛擬環境,再進行套件安裝。

容易混淆的虛擬環境

Poetry 主動納入虛擬環境管理算是立意良善,相當於把pip+venv兩者的功能直接整合在一起,但也帶來一定的複雜度,尤其在你已經自行使用了venvvirtualenvpyenv-virtualenvconda等工具來管理虛擬環境的情況下!

沒錯,Python 的虛擬環境管理就是這麼麻煩!

個人建議,對新手而言,於 Poetry 的專案中,一律使用 Poetry 來管理虛擬環境即可。我目前也是這樣,省得麻煩。

以指令建立虛擬環境

使用指令poetry env use python

❯ poetry env use python
Creating virtualenv poetry-demo-IEWSZKSE-py3.8 in /Users/kyo/Library/Caches/pypoetry/virtualenvs
Using virtualenv: /Users/kyo/Library/Caches/pypoetry/virtualenvs/poetry-demo-IEWSZKSE-py3.8

可以看出 Poetry 為我們建立了名為poetry-demo-IEWSZKSE-py3.8的虛擬環境。

重點說明

  • poetry env use python建立虛擬環境所使用的 Python 版本,取決於python指令在你的 PATH 是連結到哪個版本。同理,你也可以將指令明示為use python3use python3.8,只要這些指令確實存在 PATH 中。
  • 預設上,Poetry 會統一將虛擬環境建立在「特定目錄」裡,比如本例中存放的路徑是/Users/kyo/Library/Caches/pypoetry/virtualenvs
  • 虛擬環境的命名模式為專案名稱-亂數-Python版本

老實說我個人不是很喜歡這樣的做法,因為這意味著單一專案允許建立複數個虛擬環境(比如 Python 3.7、3.8、3.9 可以各來一個),彈性之餘也增加了混亂的可能,而且這命名模式我也不太欣賞,顯得過於僵化且冗長。

既然 Python 的虛擬環境理論上都是高度綁定專案本身的,我更偏好venv式的做法,也就是把虛擬環境放到專案目錄內,而非統一放在獨立的目錄,讓虛擬環境與專案呈現直觀的一對一關係

所幸,Poetry 具備這樣的選項。


修改config,建立專案內的.venv虛擬環境

我們先使用poetry config指令來查看 Poetry 目前幾個主要的設定,需要--list這個參數:

❯ poetry config --list
cache-dir = "/Users/kyo/Library/Caches/pypoetry"
experimental.new-installer = true
installer.parallel = true
virtualenvs.create = true
virtualenvs.in-project = false
virtualenvs.path = "{cache-dir}/virtualenvs"

其中virtualenvs.create = true若改成false,則可以停止 Poetry 在「偵測不到虛擬環境時會自行建立」的行為模式,但建議還是不要更動。

virtualenvs.in-project = false就是我們要修改的目標,使用指令:

poetry config virtualenvs.in-project true

好,我們先把之前建立的虛擬環境刪除:

❯ poetry env remove python
Deleted virtualenv: /Users/kyo/Library/Caches/pypoetry/virtualenvs/poetry-demo-IEWSZKSE-py3.8

重新建立,看看行為有何差異:

❯ poetry env use python
Creating virtualenv poetry-demo in /Users/kyo/Documents/code/poetry-demo/.venv
Using virtualenv: /Users/kyo/Documents/code/poetry-demo/.venv

可以看出:

  • 虛擬環境的路徑改為「專案的根目錄」。
  • 名稱固定為.venv

我覺得這樣的設定更加簡潔。

啟動與退出虛擬環境

啟動虛擬環境,需移至專案目錄底下,使用指令poetry shell

❯ poetry shell
Spawning shell within /Users/kyo/Documents/code/poetry-demo/.venv
❯ . /Users/kyo/Documents/code/poetry-demo/.venv/bin/activate

poetry shell指令會偵測當前目錄或所屬上層目錄是否存在pyproject.toml來確定所要啟動的虛擬環境,所以如果不移至專案目錄,則會出現下列錯誤:

❯ poetry shell

  RuntimeError

  Poetry could not find a pyproject.toml file in /Users/kyo/Documents/code or its parents

  at ~/Library/Application Support/pypoetry/venv/lib/python3.8/site-packages/poetry/core/factory.py:369 in locate
      365│             if poetry_file.exists():
      366│                 return poetry_file
      367│
      368│         else:
    → 369│             raise RuntimeError(
      370│                 "Poetry could not find a pyproject.toml file in {} or its parents".format(
      371│                     cwd
      372│                 )
      373│             )

可以看到,Poetry 的錯誤訊息非常清楚,讓你很容易知曉修正的方向,這是作為一個優秀命令列工具的必要條件。

退出就簡單多了,只需要exit即可。


Poetry 常用指令

Poetry 是一個獨立的命令列工具,就像 pyenv,它有自己的指令,需要花費額外的心力學習,且較 pip 更加複雜,這可能是使用 Poetry 的第二道關卡。好在常用的指令,其實也不超過 10 個,下面就來一一介紹。

在此我們繼續使用前面提過的 Flask 和 Black 套件,來示範並說明 Poetry 的優勢以及它和 pip 的不同之處。

Poetry 新增套件

使用指令:

poetry add

相當於pip install,我們來試著安裝 Flask 看看會有什麼變化:

img

圖中可以看出 Poetry 漂亮的命令列資訊呈現,會清楚告知總共新增了幾個套件。

此時專案中的pyproject.toml也會發生變化:

...
[tool.poetry.dependencies]
python = "^3.8"
Flask = "^2.1.1"  # 新增部分

[tool.poetry.dev-dependencies]

[build-system]
...

這裡要說明,安裝 Flask,則pyproject.toml就只會新增記載Flask = "^2.1.1"這個 top-level 的 package 項目,其餘的依賴套件不會直接記錄在toml檔中。

我覺得這是一大優點,方便區分哪些是你主動安裝的主要套件,而哪些又是基於套件的依賴關係而一併安裝的依賴套件。

poetry.lock 與更新順序

除了更新pyproject.toml,此時專案中還會新增一個檔案,名為poetry.lock,它實際上就相當於 pip 的requirements.txt,詳細記載了所有安裝的套件與版本。

當你使用poetry add指令時,Poetry 會自動依序幫你做完這三件事:

  1. 更新pyproject.toml
  2. 依照pyproject.toml的內容,更新poetry.lock
  3. 依照poetry.lock的內容,更新虛擬環境。

由此可見,poetry.lock的內容是取決於pyproject.toml,但兩者並不會自己連動,一定要基於特定指令才會進行同步與更新,poetry add就是一個典型案例。

此時專案目錄結構如下:

poetry-demo
├── poetry.lock
└── pyproject.toml

0 directories, 2 files

更新 poetry.lock

當你自行修改了pyproject.toml內容,比如變更特定套件的版本(這是有可能的,尤其在手動處理版本衝突的時候),此時poetry.lock的內容與pyproject.toml出現了「脫鉤」,必須讓它依照新的pyproject.toml內容更新、同步,使用指令:

poetry lock

如此一來,才能確保手動修改的內容,也更新到poetry.lock中,畢竟虛擬環境如果要重新建立,是基於poetry.lock的內容來安裝套件,而非pyproject.toml

還是那句話:poetry.lock相當於 Poetry 的requirements.txt


安裝套件至 dev-dependencies

有些套件,比如pytestflake8等等,只會在開發環境中使用,產品的部署環境並不需要。

Poetry 允許你區分這兩者,將上述的套件安裝至dev-dependencies區塊,方便讓你輕鬆建立一份「不包含」dev-dependencies開發套件的安裝清單

在此以 Black 為例,安裝方式如下:

poetry add black -D

poetry add black --dev

結果的區別顯示在pyproject.toml裡:

...
[tool.poetry.dependencies]
python = "^3.8"
Flask = "^2.1.1"

[tool.poetry.dev-dependencies]
black = "^22.3.0"
...

可以看到black被列在不同區塊:tool.poetry.dev-dependencies

強烈建議:善用 dev-dependencies

善用-D參數,明確區分開發環境專用的套件,我認為非常必要

首先,這些套件常常屬於「檢測型」工具,相關的依賴套件著實不少!比如flake8,它依賴了pycodestylepyflakesmccabe等等,還有blackpre-commit,依賴套件數量也都很可觀。

其次,既然它們都只在開發階段才需要,則完全可以從部署環境中缺席。如果不分青紅皂白一律安裝到dependencies區塊,部署環境容易顯得過於臃腫

常見的dev-dependencies區塊項目,例示如下:

[tool.poetry.dev-dependencies]
flake8 = "4.0.1"
yapf = "0.32.0"
pytest = "7.1.2"
pytest-django = "4.5.2"
pytest-cov = "3.0.0"
pytest-env = "0.6.2"
pytest-sugar = "0.9.4"
pre-commit = "2.20.0"

列出全部套件清單

類似pip list,這裡要使用poetry show

❯ poetry show
black              22.3.0 The uncompromising code formatter.
click              8.1.3  Composable command line interface toolkit
flask              2.1.2  A simple framework for building complex web applications.
importlib-metadata 4.11.4 Read metadata from Python packages
itsdangerous       2.1.2  Safely pass data to untrusted environments and back.
jinja2             3.1.2  A very fast and expressive template engine.
markupsafe         2.1.1  Safely add untrusted strings to HTML/XML markup.
mypy-extensions    0.4.3  Experimental type system extensions for programs checked...
pathspec           0.9.0  Utility library for gitignore style pattern matching of ...
platformdirs       2.5.2  A small Python module for determining appropriate platfo...
...

特別提醒的是,這裡的清單內容並不是來自於虛擬環境,這點和 pip 不同,而是來自於poetry.lock的內容。

你可能會想,來自於poetry.lock或虛擬環境,有差嗎?兩者不是應該要一致?

沒錯,理論上是,但也有不一致的時候,比如你使用了pip install指令安裝套件,就不會記載在poetry.lock中,那poetry show自然也不會顯示。

「樹狀」顯示套件依賴層級

Poetry 最為人津津樂道的就是它的樹狀顯示——poetry show --tree

❯ poetry show --tree
flask 2.1.1 A simple framework for building complex web applications.
├── click >=8.0
│   └── colorama *
├── importlib-metadata >=3.6.0
│   └── zipp >=0.5
├── itsdangerous >=2.0
├── jinja2 >=3.0
│   └── markupsafe >=2.0
└── werkzeug >=2.0
black 22.3.0 The uncompromising code formatter.
├── click >=8.0.0
│   └── colorama *
├── mypy-extensions >=0.4.3
├── pathspec >=0.9.0
├── platformdirs >=2
├── tomli >=1.1.0
└── typing-extensions >=3.10.0.0

讓主要套件與其依賴套件的關係與層次,一目瞭然

而且很貼心的是,它也可以**只顯示「指定套件」**的依賴層級,以celery為例:

❯ poetry show celery --tree
celery 4.4.0 Distributed Task Queue.
├── billiard >=3.6.1,<4.0
├── kombu >=4.6.7,<4.7
│   ├── amqp >=2.6.0,<2.7
│   │   └── vine >=1.1.3,<5.0.0a1
│   └── importlib-metadata >=0.18
│       ├── typing-extensions >=3.6.4
│       └── zipp >=0.5
├── pytz >0.0-dev
└── vine 1.3.0

Poetry 移除套件

使用poetry remove指令。和poetry add一樣,可以加上-D參數來移除置於開發區的套件。

而移除套件時的「依賴解析(相依性管理)」能力,正是 Poetry 遠優於 pip 的主要環節,因為 pip 沒有嘛!也是我提議改用 Poetry 的關鍵理由——為了順利移除套件

前面已經提過,pip 的pip uninstall只會移除你所指定的套件,而不會連同依賴套件一起移除。

這是基於安全考量,因為 pip 沒有「依賴解析」功能。如果貿然移除所有「安裝時一併安裝」的依賴套件,可能會造成巨大災難,讓別的套件失去效用。

前面也舉了 Flask 和 Black 都共同依賴click這個套件的例子,在手動移除套件的情況下,你可能未曾注意 Black 也依賴了click,結果為了「徹底移除」Flask 的所有相關套件,不小心把click也移除掉了。

所以,使用 pip 時,我們鮮少會去移除已經不再使用的套件。畢竟依賴關係錯綜複雜,移除套件可能造成許多「副作用」,實在是太麻煩了。

poetry remove的依賴解析

好,解釋了很多,接下來就是 Poetry 的表演了,它會幫你處理這些棘手的「套件相依性」難題,讓你輕鬆移除 Flask 而不影響 Black:

poetry remove flaskpoetry remove flask

可以對比上面安裝 Flask 時的截圖,那時總共安裝了 8 個套件,但現在移除的卻只有 7 個——沒錯,因為有依賴解析,Poetry 知道 Black 還需要click!所以不能移除:

❯ poetry show --tree
black 22.3.0 The uncompromising code formatter.
├── click >=8.0.0
│   └── colorama *
├── mypy-extensions >=0.4.3
├── pathspec >=0.9.0
├── platformdirs >=2
├── tomli >=1.1.0
└── typing-extensions >=3.10.0.0

一個套件直到環境中的其餘套件都不再依賴它,Poetry 才會安心讓它被移除。


輸出 Poetry 虛擬環境的 requirements.txt

理論上,全面改用 Poetry 後,專案中是不需要存在requirements.txt,因為它的角色已經完全被poetry.lock所取代。

但事實是,你可能還是需要它,甚至希望它隨著poetry.lock的內容更新!至少對我而言就是如此,我在 Docker 部署環境中並不使用 Poetry,所以我需要一份完全等價於poetry.lockrequirements.txt,用於 Docker 部署。

你可能想說,那我就在 Poetry 的虛擬環境下,使用以往熟悉的指令pip freeze > requirements.txt來產生一份就可以了吧?我本來也是這麼想,但實際的產出卻是如此:(提醒:目前 poetry-demo 專案中僅剩下 Black 和它的依賴套件

black @ file:///Users/kyo/Library/Caches/pypoetry/artifacts/11/4c/fc/cd6d885e9f5be135b161e365b11312cff5920d7574c8446833d7a9b1a3/black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl
click @ file:///Users/kyo/Library/Caches/pypoetry/artifacts/f0/23/09/b13d61d1fa8b3cd7c26f67505638d55002e7105849de4c4432c28e1c0d/click-8.1.2-py3-none-any.whl
mypy-extensions @ file:///Users/kyo/Library/Caches/pypoetry/artifacts/b6/a0/b0/a5dc9acd6fd12aba308634f21bb7cf0571448f20848797d7ecb327aa12/mypy_extensions-0.4.3-py2.py3-none-any.whl
...

這呈現好像不是我們以前熟悉的那樣:

black==22.3.0
click==8.1.2
mypy_extensions==0.4.3
...

沒錯,只要是使用poetry add安裝的套件,在pip freeze就會變成這樣。此時想輸出類似requirements.txt的格式,需要使用poetry export

預設的輸出結果會有 hash 值,很乾擾閱讀。不想納入 hash 則要加上參數去除。以下就是我固定用來輸出requirements.txt的指令與參數:

poetry export -f requirements.txt -o requirements.txt --without-hashes

2022/08/24補充:網友提醒,hash 有其價值,並建議保留,詳見留言區

我們再看一下輸出結果,雖然不盡相同,但也相去不遠了……嗎?等等,怎麼是空白?

輸出 dev-dependencies

因為poetry export預設只會輸出toml中的[tool.poetry.dependencies]區塊的套件!還記得上面我們把 Black 安裝到[tool.poetry.dev-dependencies]了嗎?

顯然 Poetry 認為你 export 基本上就為了部署,並不需要開發區的套件。

這倒是沒錯,不過基於演示需求,我們必須輸出[tool.poetry.dev-dependencies]的套件,才能看到 Black。

加上--dev參數即可:

poetry export -f requirements.txt -o requirements.txt --without-hashes --dev

輸出的requirements.txt內容:

black==22.3.0; python_full_version >= "3.6.2"
click==8.1.2; python_version >= "3.7" and python_full_version >= "3.6.2"
colorama==0.4.4; python_version >= "3.7" and python_full_version >= "3.6.2" and platform_system == "Windows"
...

雖然長得有點不一樣,但這個檔案確實是可以pip install的。

從這裡也可以看出先前一再提及「區分開發、部署套件」的價值——大部分時候我們並不需要輸出開發用套件。

poetry export所有參數用法與說明,請參考文件

此時專案目錄結構如下:

poetry-demo
├── poetry.lock
├── pyproject.toml
└── requirements.txt

0 directories, 3 files

Poetry 常用指令清單

算來算去,Poetry 的常用指令主要有下面幾個:

  • poetry add
  • poetry remove
  • poetry export
  • poetry env use
  • poetry shell
  • poetry show
  • poetry init
  • poetry install

其中一半,單一專案可能只會用個一兩次而已,比如initinstallenv use,實際上需要學習的指令並不多。

那麼,只要知曉這些指令,就可以順利運用 Poetry 了嗎?可能是,也可能否,所以我下面還會再補充 Poetry 的常見使用情境與操作方式,讓你接納 Poetry 的阻力可以進一步下降!


Poetry 常見使用情境與操作 QA

這部分會以「使用場景」的角度切入,介紹 Poetry 應用情境與操作說明,還包括一些自問自答:

  1. 新增專案並使用 Poetry
  2. 現有專案改用 Poetry
  3. 在別臺主機回復專案狀態
  4. 我想要重建虛擬環境
  5. 為什麼我不在 Docker 環境中使用 Poetry?
  6. 我可以使用自己習慣的 virtualenv 嗎?

一、新增專案並使用 Poetry

這是最理想的狀態,沒有過去的「包袱」,可謂是最能輕鬆採用 Poetry 的情境。

使用順序不外乎是:

  1. poetry init:初始化,建立pyproject.toml
  2. poetry env use python:建立專案虛擬環境並使用。
  3. poetry shell:進入專案但虛擬環境還未啟動,以這個指令啟動。如果使用本指令時虛擬環境尚未建立或已移除,則會直接自動幫你建立虛擬環境並使用。
  4. poetry add:新增套件並寫入虛擬環境。必要時使用-D參數新增至 dev 區塊。
  5. poetry remove:移除套件,若是移除 dev 區塊的套件,需要加上-D參數。

這部分和前面內容沒有差別,因為前面內容就是以全新專案作為基礎。

二、現有專案改用 Poetry

極為常見的需求,但並沒有很正式的做法,因為不存在poetry import之類的指令。

首先要考量的就是:要怎麼把requirements.txt的所有項目加到pyproject.toml中呢?經過一番 Google,基本上只能土法煉鋼

cat requirements.txt | xargs poetry add

然而這樣做是有可能遇到一些問題的,因為 Poetry 對套件的版本衝突比較敏感,所以即便用pip install -r requirements.txt都能正常安裝,透過上述指令的遷移過程卻仍有機會出現錯誤。

那怎麼辦?只能照著錯誤訊息手動修正requirements.txt中的套件版本。

只能說這個「手動 import」做法實在是不得已,因為我們最早介紹pyproject.toml時有提到,poetry add只會在pyproject.toml中寫入「主套件」,但這樣的 import 方式相當於把requirements.txt中的所有套件,都當作主套件add了!

畢竟在requirements.txt無從區分主套件與依賴套件,都是「一視同仁」地列出。

但如此做法也讓專案的套件失去主從之分,這樣會有什麼壞處?日後要移除主套件時,需要花額外的心力去區分主從(因為僅僅移除依賴套件並不會有移除效果),比如使用poetry show --tree去一個一個檢視,終究是件麻煩事。

完成轉換後,為保險起見,建議透過新的pyproject.toml來重建一個虛擬環境。

三、在別臺主機上重現專案的 Poetry 虛擬環境

這也是非常常見的需求。

第一步當然是git clone專案,此時專案中已經有 Poetry 所需的必要資訊了——也就是pyproject.tomlpoetry.lock

你還缺少的僅僅是虛擬環境。如果是全新的主機,則還得先安裝、設定好 Poetry。

確定 Poetry 可正常使用後,移至專案目錄底下,依序執行指令:

  1. poetry env use python:建立專案虛擬環境並使用。如果你懶得打這麼長的指令,直接poetry shell也是可以。此時我們會有一個「空的」虛擬環境。
  2. poetry install:因為是舊專案,不需要init,會直接依poetry.lock記載的套件版本安裝到虛擬環境中!類似npm install

四、我想要重建虛擬環境

在使用專案內虛擬環境方案,也就是.venv的前提下,想要刪除這個虛擬環境並加以重建,也不需要使用poetry env remove python指令了,因為會出錯。

還有更簡單暴力的方式,是什麼呢?——直接刪除.venv資料夾即可。

然後再poetry env use pythonpoetry shell建一個新的就好。

五、為什麼我不在 Docker 環境中使用 Poetry?

因為啟動容器後需要先安裝 Poetry 到全域,或打包一個帶有 Poetry 的 image,兩者都會增加新的耦合與依賴,我覺得並不妥當。

所幸 Poetry 依舊可以輸出requirements.txt,Docker 部署環境就繼續使用這個舊方案即可,而且 Poetry 本來主要就是用於「開發」時的套件管理,對部署差別不大。

六、我可以使用自己習慣的 virtualenv 嗎?

當然可以。

不過我本來也繼續使用pyenvvirtualenv,但兩者有時候也是會小小打架,後來還是索性用 Poetry 的虛擬環境就好。

一個專案對應一個虛擬環境,應該還是比較簡潔的做法,我的觀察啦!😎


結語:井然有序的複雜

總的來說,Poetry 是一款優秀的套件管理工具,但並不像 pip 那般簡單、好上手。

使用 Poetry 來管理專案的套件與虛擬環境,需要一定的學習成本,但帶來的效益還是相當可觀的,尤其在你希望能夠乾淨且安心地移除套件之際,可謂莫它莫屬。

所以,別再猶豫,從今天起,加入 Poetry 的行列吧!

實戰 Fil 改善 Python 記憶體用量

https://myapollo.com.tw/blog/fil-memory-usage-profiler/

用 Python resource 模組找出尖峰記憶體用量 一文介紹如何透過 Python resource 模組瞭解尖峰記憶體用量(peak memory usage), 不過該模組並無法提供更詳細的記憶體統計資料,無法得知具體哪部分的 Python 程式消耗大量記憶體,因此我們需要透過工具剖析(profiling)詳細的記憶體用量,以幫助定位問題之所在。

本文將介紹如何使用 Fil 剖析 Python 程式的記憶體用量,並透過 1 個簡單的範例,實際定位程式中耗用記憶體的部分,並進行改善優化。

本文環境

  • macOS 11.6
  • Python 3.7
  • pandas 1.0.3
  • Fil
$ pip install pandas==1.0.3 filprofiler

測試資料

本文需要使用以下資料集進行測試,該資料集包含至少 200MB 以上的財經新聞 CSV 檔,我們用該資料來模擬實際處理大量資料時,記憶體被耗用的情況:

Daily Financial News for 6000+ Stocks

Fil (aka Fil profiler)

Fil 是 1 套十分易於使用的 Python 記憶體剖析工具,該工具運作原理為透過作業系統所提供的 LD_PRELOAD / DYLD_INSERT_LIBRARIES 機制,在程式啟動之前載入開發者指定的共享函式庫(shared library), 該函式庫可以覆寫(override)某些重要的系統 API, 使得開發者可透過該共享函式庫攔截(intercept)某些 API 的呼叫,例如 Fil 透過覆寫記憶體相關的底層 API, 使得 Fil 得以攔截並統計記憶體分配以及使用情況,達成記憶體剖析的功能。

如果想了解 Fil 會攔截哪些 API, 可以參考 What Fil tracks 一文。

Fil 初體驗

以下範例載入測試資料集中的 raw_partner_headlines.csv, 並查詢多少新聞標題中含有 J.P. Morgan 以及該新聞中所提到的股票代號,並且匯總股票代號出現的總次數,最後只顯示超過 5 次的股票代號:

import pandas as pd


def load_data():
    return pd.read_csv('<path to>/raw_partner_headlines.csv')


def main():
    df = load_data()
    df = df.query('headline.str.contains("J.P. Morgan")', engine='python')
    df = df.groupby(by='stock').count()
    for r in df.sort_values(by='headline', ascending=False).itertuples():
        if r.url > 5:
            print(r.Index, r.url)


main()

上述範例執行結果如下,可以看到被 J.P. Morgan 提到最多次的股票代號為 JPM, 其次為 ARNA:

$ python test.py
JPM 15
ARNA 10
ZIOP 8
BMRN 8
AVEO 8
TEX 6
MDVN 6
NKTR 6
REGN 6
VVUS 6

接著我們實際用 Fil 剖析上述範例會用到多少記憶體,以下是呼叫 fil-profiletest.py 進行記憶體用量剖析的指令:

$ fil-profile run test.py
=fil-profile= Memory usage will be written out at exit, and opened automatically in a browser.
...(略)...

一旦 fil-profile 指令完成記憶體剖析的工作, Fil 就會打開 1 個瀏覽器視窗,並且將分析報告以網頁方式呈現:

img

上圖顯示前文的範例用了 732.6 MiB 的記憶體,相較於 raw_partner_headlines.csv 檔案大小 400MB 而言,多耗用將近 1 倍的記憶體,理想情況是載入資料之後只佔用 400MB的記憶體空間,但由於 Python 的物件化設計,記憶體耗用的情況是預料之中的事,可以想見一旦 raw_partner_headlines.csv 檔案越大,就需要記憶體越大的機器進行運算,最終勢必對成本(例如機器租用成本)造成影響,所以我們需要優化記憶體的使用。

接著,我們可以循著報告往下找到記憶體耗用的癥結點:

img

上圖可以發現 main() 函式佔了約 84.59% 的記憶體用量,但具體在 main() 函式的哪部分,可以繼續往下追蹤:

img

上圖顯示 main() 函式中的 load_data() 函式佔用約 66.38% 的記憶體用量,看來 load_data() 的記憶體用量優化,是 1 個可以進行的方向。

實戰優化

pandas.read_csv() 預設會載入整份資料到記憶體之中,如果能夠分批載入計算將可以有效降低記憶體用量,所以我們可以為 read_csv() 設定 chunksize 參數,讓其分批載入資料,不過分批載入資料的缺點是無法一次計算好最終結果,必須將每批資料的計算結果加總儲存,也就是下列範例中的 counter[r.Index] += r.url 部分,最終再將加總後的結果列印:

import pandas as pd

from collections import Counter


def load_data():
    return pd.read_csv(
        '<path to>/raw_partner_headlines.csv',
        chunksize=1000
    )


def main():
    counter = Counter()
    for df in load_data():
        df = df.query('headline.str.contains("J.P. Morgan")', engine='python')
        df = df.groupby(by='stock').count()
        for r in df.itertuples():
            counter[r.Index] += r.url

    for stock, count in counter.items():
        if count > 5:
            print(stock, count)


main()

上述範例執行結果如下:

$ python test.py
ARNA 10
AVEO 8
BMRN 8
JPM 15
MDVN 6
NKTR 6
REGN 6
TEX 6
VVUS 6
ZIOP 8

其記憶體剖析報告如下:

img

上述報告可以看到,僅僅只是加個參數,就能夠讓記憶體用量從 732.6 MiB 下降至 150.2 MiB, 下降幅度相當可觀,對於更大的資料也不須擔心記憶體不足的問題,如果是租用雲端機器,也只需要相當低規格的機器即可達成。

以上就是透過 Fil 實際改善記憶體用量的實戰過程。

Happy coding!

References

https://pythonspeed.com/fil/docs/index.html

臺股個股線圖繪製

https://hackmd.io/@s02260441/Hki9NN5jL

import pandas as pd
import datetime as datetime
import matplotlib
import mplfinance as mpf
import pandas_datareader as pdr

# 導入pandas、matplotlib、mplfinance模組,將mplfinance模組縮寫為mpf
# 這邊要導入matplotlib的原因是因為mplfinance繪圖時需要調用mptplotlib模組

target_stock = "2330.TW"  # 設定要繪製走勢圖的股票
start = datetime.datetime(2018, 4, 1)
df = pdr.DataReader("2330.TW", "yahoo", start=start)
print(df)


mc = mpf.make_marketcolors(up="r", down="g", inherit=True)
s = mpf.make_mpf_style(base_mpf_style="yahoo", marketcolors=mc)
# 針對線圖的外觀微調,將上漲設定為紅色,下跌設定為綠色,符合臺股表示習慣
# 接著把自訂的marketcolors放到自訂的style中,而這個改動是基於預設的yahoo外觀

kwargs = dict(
    type="candle",
    mav=(5, 20, 60),
    volume=True,
    figratio=(10, 8),
    figscale=0.75,
    title=target_stock,
    style=s,
)
# 設定可變參數kwargs,並在變數中填上繪圖時會用到的設定值

mpf.plot(df, **kwargs)
# 選擇df資料表為資料來源,帶入kwargs參數,畫出目標股票的走勢圖

https://yhhuang1966.blogspot.com/2022/09/python-mplfinance.html

# https://python.plainenglish.io/plot-stock-chart-using-mplfinance-in-python-9286fc69689
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
import mplfinance as mpf

# download stock price data
symbol = "AAPL"
df = yf.download(symbol, period="6mo")

# Add MACD as subplot
def MACD(df, window_slow, window_fast, window_signal):
    macd = pd.DataFrame()
    macd["ema_slow"] = df["Close"].ewm(span=window_slow).mean()
    macd["ema_fast"] = df["Close"].ewm(span=window_fast).mean()
    macd["macd"] = macd["ema_slow"] - macd["ema_fast"]
    macd["signal"] = macd["macd"].ewm(span=window_signal).mean()
    macd["diff"] = macd["macd"] - macd["signal"]
    macd["bar_positive"] = macd["diff"].map(lambda x: x if x > 0 else 0)
    macd["bar_negative"] = macd["diff"].map(lambda x: x if x < 0 else 0)
    return macd


macd = MACD(df, 12, 26, 9)
macd_plot = [
    mpf.make_addplot(
        (macd["macd"]), color="#606060", panel=2, ylabel="MACD", secondary_y=False
    ),
    mpf.make_addplot((macd["signal"]), color="#1f77b4", panel=2, secondary_y=False),
    mpf.make_addplot((macd["bar_positive"]), type="bar", color="#4dc790", panel=2),
    mpf.make_addplot((macd["bar_negative"]), type="bar", color="#fd6b6c", panel=2),
]

mpf.plot(df, type="candle", volume=True, addplot=macd_plot)
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
import mplfinance as mpf
import talib as ta

# download stock price data
symbol = "AAPL"
df = yf.download(symbol, start="2021-01-01", end="2022-05-01")
for num in [10, 120]:
    df[f"SMA{num}"] = ta.SMA(df["Close"], timeperiod=num)

df["CLOSE_LINEARREG_ANGLE"] = ta.LINEARREG_ANGLE(df["Close"], timeperiod=14)
df["SMA_LINEARREG_ANGLE"] = ta.LINEARREG_ANGLE(df["SMA10"], timeperiod=14)
df = df.dropna()

add_plot = [
    mpf.make_addplot(df["CLOSE_LINEARREG_ANGLE"]),
    mpf.make_addplot(df["SMA_LINEARREG_ANGLE"]),
]
mc = mpf.make_marketcolors(up="r", down="g", inherit=True)
mpf.plot(
    df,
    type="candle",
    volume=True,
    style=mpf.make_mpf_style(base_mpf_style="yahoo", marketcolors=mc),
    addplot=add_plot,
)
from binance.client import Client
import pandas as pd
import matplotlib.pyplot as plt
import mplfinance as mpf
import talib as ta
import datetime as dt
import json
import os


class binanceAPI:
    def __init__(self, configPath):
        with open(configPath, "r") as f:
            self.kw_login = json.loads(f.read())
        self.api = self.__login(self.kw_login["PUBLIC"], self.kw_login["SECRET"])

    def __login(self, PUBLIC, SECRET):
        return Client(api_key=PUBLIC, api_secret=SECRET)


def build_df(klines):
    cols = [
        "timestamp",
        "open",
        "high",
        "low",
        "close",
        "volume",
        "close_time",
        "quote_av",
        "trades",
        "tb_base_av",
        "tb_quote_av",
        "ignore",
    ]
    df = pd.DataFrame(klines, columns=cols)
    df["timestamp"] = [dt.datetime.fromtimestamp(x / 1000.0) for x in df["timestamp"]]
    df.set_index("timestamp", inplace=True)
    df = df[["open", "high", "low", "close", "volume"]]
    df[["open", "high", "low", "close", "volume"]] = df[
        ["open", "high", "low", "close", "volume"]
    ].astype(float)
    for num in [5, 10, 120]:
        df[f"SMA{num}"] = ta.SMA(df["close"], timeperiod=num)
    df["SMA_LINEARREG_ANGLE"] = ta.LINEARREG_ANGLE(df["SMA5"], timeperiod=14)
    df["CLOSE_LINEARREG_ANGLE"] = ta.LINEARREG_ANGLE(df["close"], timeperiod=14)
    df = df.dropna()
    df["idx"] = range(0, len(df))
    return df


if __name__ == "__main__":
    client = binanceAPI(os.environ["HOME"] + f"/.mybin/jason/binance_login.txt")
    KLINE_INTERVAL = Client.KLINE_INTERVAL_30MINUTE
    start_time = dt.datetime(2022, 11, 1, hour=8, minute=00, second=0)
    end_time = dt.datetime.now()

    klines = client.api.get_historical_klines(
        symbol="BTCUSDT",
        interval=KLINE_INTERVAL,
        start_str=start_time.strftime("%Y-%m-%d %H:%M:%S"),
        end_str=end_time.strftime("%Y-%m-%d %H:%M:%S"),
    )
    df = build_df(klines)
    print(df.to_markdown())
    input()
    add_plot = [
        mpf.make_addplot(df["CLOSE_LINEARREG_ANGLE"]),
        mpf.make_addplot(df["SMA_LINEARREG_ANGLE"]),
    ]
    mc = mpf.make_marketcolors(up="r", down="g", inherit=True)
    mpf.plot(
        df,
        type="candle",
        volume=True,
        style=mpf.make_mpf_style(base_mpf_style="yahoo", marketcolors=mc),
        addplot=add_plot,
    )

# Function.py
# 載入套件
import yfinance as yf
import mplfinance as mpf
import numpy as np

# 透過Yfinance取得K棒歷史資料
def GetKBar(SDate, EDate, Prod, Kind, Cycle):
    # 轉換日期格式
    SDate = SDate[:4] + "-" + SDate[4:6] + "-" + SDate[6:]
    EDate = EDate[:4] + "-" + EDate[4:6] + "-" + EDate[6:]
    # 指數前面要加 ^ 符號
    if Kind == "Index":
        Prod = "^" + Prod
    # 從 yahoo finance 下載資料
    Data = yf.download(Prod, start=SDate, end=EDate, interval=Cycle)
    # 將欄位名稱改為英文小寫
    Data.columns = [i.lower() for i in Data.columns]
    # 因python會有小數點精確度問題,故將股價取到小數後兩位
    Data.open = [round(i, 2) for i in Data.open]
    Data.high = [round(i, 2) for i in Data.high]
    Data.low = [round(i, 2) for i in Data.low]
    Data.close = [round(i, 2) for i in Data.close]
    return Data


# 圖片物件
class DrawKBar:
    # 初始設定
    def __init__(self, KBar):
        self.KBar = KBar
        self.TableList = []

    # 新增附圖
    def Add(
        self,
        data,
        panel=0,
        type="line",
        marker=".",
        color="black",
        scatter=False,
        ylabel="",
    ):
        # Table = mpf.make_addplot(data,panel=panel,type=type,color=color)
        Table = mpf.make_addplot(
            data,
            panel=panel,
            type=type,
            marker=marker,
            color=color,
            scatter=scatter,
            ylabel=ylabel,
            secondary_y=False,
        )
        self.TableList.append(Table)

    # 顯示圖片
    def Show(self):
        KBar_color = mpf.make_marketcolors(
            up="red", down="green", edge="inherit", wick="inherit", volume="inherit"
        )
        KBar_style = mpf.make_mpf_style(
            base_mpf_style="yahoo", edgecolor="black", marketcolors=KBar_color
        )
        mpf.plot(
            self.KBar,
            type="candle",
            style=KBar_style,
            volume=True,
            addplot=self.TableList,
        )


# 計算績效KPI
def GetKPI(ProfitList):
    # 將 List 轉為 numpy array 格式
    ProfitList = np.array(ProfitList)
    print()

    # 交易次數
    TotalNum = len(ProfitList)
    print("交易次數:", TotalNum, "次")

    # 總損益
    TotalProfit = round(sum(ProfitList), 2)
    print("總損益:", TotalProfit, "元")

    # 平均損益
    if TotalNum == 0:
        AvgProfit = None
    else:
        AvgProfit = round(TotalProfit / TotalNum, 2)
    print("平均損益:", AvgProfit, "元")

    # 總勝率
    Win = [i for i in ProfitList if i > 0]  # 獲利的部分
    Loss = [i for i in ProfitList if i < 0]  # 虧損的部分
    if TotalNum == 0:
        WinRate = None
    else:
        WinRate = round(len(Win) / TotalNum * 100, 2)
    print("總勝率:", WinRate, "%")

    # 平均獲利
    if len(Win) == 0:
        AvgWin = None
    else:
        AvgWin = round(np.mean(Win), 2)
    print("平均獲利:", AvgWin, "元")

    # 平均虧損
    if len(Loss) == 0:
        AvgLoss = None
    else:
        AvgLoss = round(np.mean(Loss), 2)
    print("平均虧損:", AvgLoss, "元")

    # 獲利因子
    if sum(Loss) == 0:
        ProfitFactor = None
    else:
        ProfitFactor = round(sum(Win) / abs(sum(Loss)), 2)
    print("獲利因子:", ProfitFactor, "倍")

    # 最大資金回落
    MaxCapital = 0
    Capital = 0
    MDD = 0
    DD = 0
    for i in ProfitList:
        Capital += i
        MaxCapital = max(MaxCapital, Capital)
        DD = round(MaxCapital - Capital, 2)
        MDD = max(MDD, DD)
    print("最大資金回落:", abs(MDD), "元")
# python 8-2.py "20210101" "20220501"  "AAPL" ""  "1D"
# 載入套件
from plotly.offline import plot
import talib as ta
import plotly.graph_objs as go
import sys, Function

# 資料參數 (可自行調整)
SDate = sys.argv[1]  # 資料起始日
EDate = sys.argv[2]  # 資料結束日
Prod = sys.argv[3]  # 商品代碼
Kind = sys.argv[4]  # 商品種類
Cycle = sys.argv[5]  # K棒週期

# 取得K棒資料
KBar = Function.GetKBar(SDate, EDate, Prod, Kind, Cycle)
print(KBar)

# 計算技術指標
flag = False
KBar["CDL3BLACKCROWS"] = ta.CDL3BLACKCROWS(
    KBar["open"], KBar["high"], KBar["low"], KBar["close"]
)
print(KBar)
print(KBar["CDL3BLACKCROWS"].tolist(), len(KBar["CDL3BLACKCROWS"]))
for i in range(0, len(KBar["CDL3BLACKCROWS"])):
    signal = KBar.iloc[0]["CDL3BLACKCROWS"]
    if float(signal) < 0:
        print(KBar.index[i], signal)
        flag = True

if flag == False:
    print("期間內無觸發此型態訊號")

trace = go.Candlestick(  # x= pd.to_datetime(dfohlc.index.values),
    open=KBar["open"], high=KBar["high"], low=KBar["low"], close=KBar["close"]
)
data = [trace]

plot(data, filename="go_candle1.html")

取得目前週選擇代號

from multiprocessing import Process, Queue
from shioaji.contracts import Contract
from shioaji import Exchange
from time import sleep
from line_notify import LineNotify

import sys
import platform
import signal
import datetime
import shioaji as sj
import os
import json
import pandas as pd


token_list = {

}


def line_notify(message):
    for _, token in token_list.items():
        LineNotify(token).send(message)


class Watcher:
    def __init__(self):
        self.child = os.fork()
        if self.child == 0:
            return
        else:
            self.watch()

    def watch(self):
        try:
            os.wait()
        except KeyboardInterrupt:
            self.kill()
        sys.exit()

    def kill(self):
        try:
            print("kill")
            os.kill(self.child, signal.SIGKILL)
        except OSError:
            pass


def get_previous_wednesday():
    today = datetime.datetime.today()
    wednesday = (
        today - datetime.timedelta(days=today.weekday()) + datetime.timedelta(days=2)
    )
    previous_wednesday = wednesday - datetime.timedelta(days=7)
    return previous_wednesday.date()


def get_this_week_wednesday():
    today = datetime.datetime.today()
    wednesday = (today + datetime.timedelta(days=(2 - today.weekday()))).date()
    return wednesday


def get_next_week_wednesday():
    today = datetime.datetime.today()
    wednesday = (today + datetime.timedelta(days=(2 - today.weekday() + 7))).date()
    return wednesday


def get_option_symbol(api):
    for option in api.Contracts.Options:
        for contract in option:
            if "TX" in contract.category:
                now = datetime.datetime.now()
                wednesday_time = get_this_week_wednesday()
                wednesday_time = datetime.datetime.combine(
                    wednesday_time, datetime.datetime.min.time()
                ) + datetime.timedelta(
                    hours=14
                )  # 因為程式是14:50啟動計算所以改設定14:00
                # 根據當前時間判斷是否在星期三 15:00之前。如果在此時間之前,則列印上週三和本週三的日期;否則列印本週三和下週三的日期:
                if now < wednesday_time:
                    if datetime.datetime.strptime(
                        contract.update_date, "%Y/%m/%d"
                    ).date() >= get_previous_wednesday() and contract.delivery_date == get_this_week_wednesday().strftime(
                        "%Y/%m/%d"
                    ):
                        print(
                            contract.symbol,
                            contract.name,
                            contract.update_date,
                            contract.delivery_date,
                        )
                        return contract.symbol
                else:
                    if datetime.datetime.strptime(
                        contract.update_date, "%Y/%m/%d"
                    ).date() >= get_this_week_wednesday() and contract.delivery_date == get_next_week_wednesday().strftime(
                        "%Y/%m/%d"
                    ):
                        print(
                            contract.symbol,
                            contract.name,
                            contract.update_date,
                            contract.delivery_date,
                        )
                        return contract.symbol


if __name__ == "__main__":
    if platform.system().lower() == "linux":
        Watcher()

    with open(os.environ["HOME"] + "/.mybin/shioaji_token.txt", "r") as f:
        api = sj.Shioaji()
        kw_login = json.loads(f.read())
        print(kw_login)
        api.login(**kw_login, contracts_timeout=300000)
        # api.fetch_contracts(contract_download=True)
        print(get_option_symbol(api))
        api.logout()

取得週選週三換約15:00 後各履約價第一筆成交價

import platform
import shioaji as sj
import datetime as dt
import pandas as pd
import signal
import os
import sys
import json

from line_notify import LineNotify


class Watcher:
    def __init__(self):
        self.child = os.fork()
        if self.child == 0:
            return
        else:
            self.watch()

    def watch(self):
        try:
            os.wait()
        except KeyboardInterrupt:
            self.kill()
        sys.exit()

    def kill(self):
        try:
            print("kill")
            os.kill(self.child, signal.SIGKILL)
        except OSError:
            pass


# 取得大臺期貨開盤價
def getOpenPrice(api, year, month, day, open_time):
    try:
        TXF = (
            sorted([x for x in dir(api.Contracts.Futures.TXF) if x.startswith("TXF")])
        )[0]
        date = dt.datetime(year, month, day).strftime("%Y-%m-%d")
        kbars = api.kbars(api.Contracts.Futures.TXF[TXF], date)
        df = pd.DataFrame({**kbars})
        if not df.empty:
            df.ts = pd.to_datetime(df.ts)
            df.set_index("ts", inplace=True)
            return df.iloc[df.index.get_loc(open_time, method="nearest")]["Open"]
        else:
            return None
    except:
        return None


def getOptionsDealts(api, OP, year, month, day):
    date = dt.datetime(year, month, day).strftime("%Y-%m-%d")
    try:
        # print(OP[:3], OP, date)
        ticks = api.ticks(api.Contracts.Options[OP[:3]][OP], date)
        df = pd.DataFrame({**ticks})
        if df.empty:
            return pd.DataFrame()
        df.ts = pd.to_datetime(df.ts)
        df["OP"] = OP
    except:
        # print(OP)
        return pd.DataFrame()
    return df


# {1: buy deal, 2: sell deal, 0: can't judge}
def set_tick_type(df):
    if df["close"] == df["bid_price"]:
        return 1
    elif df["close"] == df["ask_price"]:
        return 2
    else:
        return 0


def get_options_contracts(api):
    option_contracts = {"week": [], "month": []}
    symbols = sorted([x.symbol for x in api.Contracts.Options.TXO])
    near_option_symbol = symbols[0][3:9]

    for option in api.Contracts.Options:
        for contract in option:
            if "TX" in contract.category and near_option_symbol in contract.symbol:
                if contract.category == "TXO":
                    option_contracts["month"].append(contract)
                else:
                    option_contracts["week"].append(contract)

    return option_contracts


def main():
    api = sj.Shioaji(simulation=False)
    with open(os.environ["HOME"] + "/.mybin/shioaji_token.txt", "r") as f:
        # with open(os.environ["HOME"] + "/.mybin/login.txt", "r") as f:
        kw_login = json.loads(f.read())
        api.login(**kw_login, contracts_timeout=300000)

    options_contracts = get_options_contracts(api)

    week_options = []
    for c in options_contracts["week"]:
        week_options.append(c["symbol"])

    today = dt.date.today()
    last_thursday = today - dt.timedelta(days=(today.weekday() + 1) % 7 + 3)

    first_row_list = []
    for OP in week_options:
        df = getOptionsDealts(
            api, OP, last_thursday.year, last_thursday.month, last_thursday.day
        )
        if not df.empty:
            columns = [
                "OP",
                "ts",
                "close",
                "volume",
                "ask_price",
                "ask_volume",
                "bid_price",
                "bid_volume",
            ]
            df = df[columns]
            df = df.assign(tick_type=df.apply(set_tick_type, axis=1))
            # 假設日期時間欄位名稱為 'datetime'
            df["ts"] = pd.to_datetime(df["ts"])  # 將欄位轉換為日期時間格式
            # 選擇15:00之後的時間
            df = df[df["ts"].dt.hour >= 15]
            df["OP_close"] = df["OP"].str.extract(r"(\d{5})[PC]")
            df["OP_close"] = df["OP_close"].astype(int)
            df["OP_close"] = df["OP_close"] + df["close"]

            if not df.empty:
                df.reset_index(drop=True, inplace=True)
                first_row_list.append(df.iloc[0])
                # print(df, df.iloc[0], type(df.iloc[0]))

    df_new = pd.DataFrame(
        first_row_list,
        columns=[
            "OP",
            "ts",
            "close",
            "volume",
            "ask_price",
            "ask_volume",
            "bid_price",
            "bid_volume",
            "tick_type",
            "OP_close",
        ],
    )
    df_new.sort_values("OP", ascending=False, inplace=True)
    df_new = df_new.reset_index(drop=True)
    print(df_new)
    api.logout()


if __name__ == "__main__":
    if platform.system().lower() == "linux":
        Watcher()
    main()

取得最近期貨合約代號

def getOpenPrice(api, year, month, day, open_time):
    TXF = (sorted([x for x in dir(api.Contracts.Futures.TXF) if x.startswith('TXF')]))[0]
    date = dt.datetime(year, month, day).strftime("%Y-%m-%d")
    kbars = api.kbars(api.Contracts.Futures.TXF[TXF], date)
    df = pd.DataFrame({**kbars})
    df.ts = pd.to_datetime(df.ts)
    df.set_index("ts", inplace=True)
    return df.iloc[df.index.get_loc(open_time, method="nearest")]["Open"]

取得週跟月選擇權合約

import shioaji as sj
import os
import json


def get_options_contracts(api):
    option_contracts = {"week": [], "month": []}
    symbols = sorted([x.symbol for x in api.Contracts.Options.TXO])
    near_option_symbol = symbols[0][3:9]

    for option in api.Contracts.Options:
        for contract in option:
            if "TX" in contract.category and near_option_symbol in contract.symbol:
                if contract.category == "TXO":
                    option_contracts["month"].append(contract)
                else:
                    option_contracts["week"].append(contract)

    return option_contracts


if __name__ == "__main__":
    with open(os.environ["HOME"] + "/.mybin/shioaji_token.txt", "r") as f:
        api = sj.Shioaji()
        kw_login = json.loads(f.read())
        api.login(**kw_login, contracts_timeout=300000)
        options_contracts = get_options_contracts(api)
        print("Month option contracts:", options_contracts["month"])
        print("Week option contracts:", options_contracts["week"])

使用 Token 版本

#
#
# 用來記錄 選擇權的 tick和報價
#
#

from multiprocessing import Process, Queue
from shioaji.contracts import Contract
from shioaji import contracts
from time import sleep


import datetime
import shioaji as sj
import os
import json
import queue


class shioaji_proxy:
    def __init__(self, queue: Queue, bool_call: bool, bool_TSE: bool):
        self.queue = queue
        self.bool_call = bool_call

        with open(os.environ["HOME"] + "/.mybin/shioaji_token.txt", "r") as f:
            self.api = sj.Shioaji()
            kw_login = json.loads(f.read())
            self.api.login(**kw_login, contracts_timeout=300000)

        # 之後改版會修正就不需要
        # self.api.fetch_contracts(contract_download=True)

        self.api.quote.set_on_bidask_fop_v1_callback(self.quote_callback)
        self.api.quote.set_on_tick_fop_v1_callback(self.quote_callback)
        option_contracts = self.get_options_contracts()

        c: Contract
        for c in option_contracts["week"]:
            if c.symbol[-1] == ("C" if self.bool_call else "P"):
                print(c)
                self.api.quote.subscribe(
                    c,
                    quote_type=sj.constant.QuoteType.Tick,
                    # version=sj.constant.QuoteVersion.v1,
                )
                self.api.quote.subscribe(
                    c,
                    quote_type=sj.constant.QuoteType.BidAsk,
                    # version=sj.constant.QuoteVersion.v1,
                )

        # self.contract: Contract = self.api.Contracts.Options.TXO.TXO202206016600C if bool_call else self.api.Contracts.Options.TXO.TXO202206016600P

        # if bool_TSE:
        #    self.api.quote.subscribe(
        #        self.api.Contracts.Indexs.TSE.TSE001,
        #        quote_type=sj.constant.QuoteType.Tick,
        #    )

    def get_options_contracts(self):
        option_contracts = {"week": [], "month": []}
        symbols = sorted([x.symbol for x in self.api.Contracts.Options.TXO])
        near_option_symbol = symbols[0][3:9]

        for option in self.api.Contracts.Options:
            for contract in option:
                if "TX" in contract.category and near_option_symbol in contract.symbol:
                    if contract.category == "TXO":
                        option_contracts["month"].append(contract)
                    else:
                        option_contracts["week"].append(contract)

        return option_contracts

    def quote_callback(self, topic: str, quote: dict):
        # print(topic, quote)
        self.queue.put((topic, quote))


def shioaji_subscriber(queue, bool_call, bool_TSE):
    proxy = shioaji_proxy(queue, bool_call, bool_TSE)

    while True:
        sleep(1)
        queue.put(f"{proxy} {datetime.datetime.now()}")


if __name__ == "__main__":
    q = Queue()  # 於主進程創建隊列物件
    process_list = []
    print("main queue id: %d" % id(q))

    proc = Process(target=shioaji_subscriber, args=(q, True, True))
    process_list.append(proc)
    proc.start()
    proc = Process(target=shioaji_subscriber, args=(q, False, False))
    process_list.append(proc)
    proc.start()

    bool_AM = (
        True
        if datetime.datetime.now().time() < datetime.time(hour=12, minute=0)
        else False
    )

    with open(
        f'TXO-{datetime.datetime.now().date()}-{"AM" if bool_AM else "PM"}.txt', "w+"
    ) as fp:
        ret_count = 0
        while True:
            try:
                ret = q.get(timeout=1)
            except queue.Empty:
                print(datetime.datetime.now())
            else:
                fp.write(f"{datetime.datetime.now()}\t{ret[0]}\t{ret[1]}\n")
                ret_count += 1

            # Check if we have written 100 records, then write to file and reset count
            if ret_count == 100:
                fp.flush()  # flush the file buffer to disk
                ret_count = 0

            # Check if current time is between 13:46 and 14:00, or between 5:01 and 5:10, then exit loop
            if (
                datetime.time(hour=13, minute=46)
                < datetime.datetime.now().time()
                < datetime.time(hour=14, minute=0)
            ) or (
                datetime.time(hour=5, minute=1)
                < datetime.datetime.now().time()
                < datetime.time(hour=5, minute=10)
            ):
                break

    for p in process_list:
        p.terminate()
        p.join()
        p.close()
        print(f"{p} joined")

用來記錄 選擇權的 tick和報價

from multiprocessing import Process, Queue

import datetime
import shioaji as sj
from shioaji import contracts
from shioaji.contracts import Contract
import os
import json
from time import sleep
import queue


class shioaji_proxy:
    def __init__(self, queue: Queue, bool_call: bool, bool_TSE: bool):
        self.queue = queue
        self.bool_call = bool_call

        with open(os.environ["HOME"] + "/.mybin/login.txt", "r") as f:
            self.api = sj.Shioaji()
            kw_login = json.loads(f.read())
            self.api.login(**kw_login, contracts_timeout=300000)

        self.api.quote.set_quote_callback(self.quote_callback)

        symbols = []
        for x in self.api.Contracts.Options.TXO:
            symbols.append(x.symbol)
        symbols.sort()
        str_option_near = symbols[0][:9]

        contracts = []
        for x in self.api.Contracts.Options.TXO:
            if x.symbol.startswith(str_option_near):
                contracts.append(x)

        c: Contract
        for c in contracts:
            if c.symbol[-1] == ("C" if self.bool_call else "P"):
                self.api.quote.subscribe(c, quote_type=sj.constant.QuoteType.Tick)
                self.api.quote.subscribe(c, quote_type=sj.constant.QuoteType.BidAsk)

        # self.contract: Contract = self.api.Contracts.Options.TXO.TXO202206016600C if bool_call else self.api.Contracts.Options.TXO.TXO202206016600P

        if bool_TSE:
            self.api.quote.subscribe(
                self.api.Contracts.Indexs.TSE.TSE001,
                quote_type=sj.constant.QuoteType.Tick,
            )

    def quote_callback(self, topic: str, quote: dict):
        # print(topic, quote)
        self.queue.put((topic, quote))


def shioaji_subscriber(queue, bool_call, bool_TSE):
    proxy = shioaji_proxy(queue, bool_call, bool_TSE)

    while True:
        sleep(1)
        queue.put(f"{proxy} {datetime.datetime.now()}")


if __name__ == "__main__":
    q = Queue()  # 於主進程創建隊列物件
    process_list = []
    print("main queue id: %d" % id(q))

    # shioaji_subscriber(q, True, True)

    proc = Process(target=shioaji_subscriber, args=(q, True, True))
    process_list.append(proc)
    proc.start()
    proc = Process(target=shioaji_subscriber, args=(q, False, False))
    process_list.append(proc)
    proc.start()

    bool_AM = (
        True
        if datetime.datetime.now().time() < datetime.time(hour=12, minute=0)
        else False
    )

    with open(
        f'TXO-{datetime.datetime.now().date()}-{"AM" if bool_AM else "PM"}.txt', "w+"
    ) as fp:
        while True:
            try:
                ret = q.get(timeout=1)
            except queue.Empty:
                pass
                print(datetime.datetime.now())
            else:
                # print(ret)
                fp.write(f"{datetime.datetime.now()}\t{ret[0]}\t{ret[1]}\n")

            if (
                datetime.time(hour=13, minute=46)
                < datetime.datetime.now().time()
                < datetime.time(hour=14, minute=0)
            ):
                break

            if (
                datetime.time(hour=5, minute=1)
                < datetime.datetime.now().time()
                < datetime.time(hour=5, minute=10)
            ):
                break

    for p in process_list:
        p.terminate()
        p.join()
        p.close()
        print(f"{p} joined")

選擇權最近合約排序

import shioaji as sj
import os
import json

with open(os.environ["HOME"] + "/.mybin/login.txt", "r") as f:
    api = sj.Shioaji()
    kw_login = json.loads(f.read())
    api.login(**kw_login, contracts_timeout=300000)

symbols = []
for x in api.Contracts.Options.TXO:
    symbols.append(x.symbol)

symbols.sort()
str_option_near = symbols[0][:9]

contracts = []
for x in api.Contracts.Options.TXO:
    if x.symbol.startswith(str_option_near):
        contracts.append(x)

for c in contracts:
    print(c, c.symbol)
    # if c.symbol[-1] == ("C" if bool_call else "P"):
    # print(x)
from shioaji import TickSTKv1, TickFOPv1, BidAskSTKv1, BidAskFOPv1, Exchange


def order_callback(stat, msg: dict):
    print(f"\n\033[1;33morder_callback: {stat} {msg}\033[0m\n")


def event_callback(resp_code, event, info, event_str):
    print(
        f"\n\033[1;33mevent_callback: {resp_code} {event} {info} {event_str}\033[0m\n"
    )


def quote_callback(topic: str, quote: dict):
    print(f"\n\033[1;33mquote_callback: {topic} {quote}\033[0m\n")


def stk_tick_callback_v1(exchange: Exchange, tick: TickSTKv1):
    print(f"stk_tick_callback_v1: {exchange} {tick}")
    print(json.dumps(tick))


def stk_bidask_callback_v1(exchange: Exchange, bidask: BidAskSTKv1):
    print(f"stk_bidask_callback_v1: {exchange} {bidask}")


# {'code': 'TXFG2', 'datetime': '2022-06-23T21:52:32.489000', 'bid_total_vol': 74, 'ask_total_vol': 44, 'bid_price': ['14933', '14932', '14931', '14930', '14929'], 'bid_volume': [3, 11, 20, 31, 9], 'diff_bid_vol': [-4, 0, -5, 4, -1], 'ask_price': ['14935', '14936', '14937', '14938', '14939'], 'ask_volume': [5, 8, 8, 11, 12], 'diff_ask_vol': [3, 3, 0, 0, 0], 'first_derived_bid_price': '0', 'first_derived_ask_price': '14939', 'first_derived_bid_vol': 0, 'first_derived_ask_vol': 1, 'underlying_price': '15176.44', 'simtrade': 0}
# {'code': 'TXFG2', 'datetime': '2022-06-23T21:52:32.407000', 'open': '14904', 'underlying_price': '15176.44', 'bid_side_total_vol': 29991, 'ask_side_total_vol': 29864, 'avg_price': '14949.592158', 'close': '14934', 'high': '15041', 'low': '14849', 'amount': '29868', 'total_amount': '695828767', 'volume': 2, 'total_volume': 46545, 'tick_type': 2, 'chg_type': 4, 'price_chg': '-5', 'pct_chg': '-0.033469', 'simtrade': 0}


def fop_tick_callback_v1(exchange: Exchange, tick: TickFOPv1):
    # print(f'fop_tick_callback_v1: {exchange} {tick}')
    print(tick.to_dict(raw=True))


def fop_bidask_callback_v1(exchange: Exchange, bidask: BidAskFOPv1):
    # print(f'fop_bidask_callback_v1: {exchange} {bidask}')
    print(bidask.to_dict(raw=True))


api.set_order_callback(order_callback)
api.quote.set_event_callback(event_callback)
api.quote.set_quote_callback(quote_callback)
api.quote.set_on_tick_stk_v1_callback(stk_tick_callback_v1)
api.quote.set_on_bidask_stk_v1_callback(stk_bidask_callback_v1)
api.quote.set_on_tick_fop_v1_callback(fop_tick_callback_v1)
api.quote.set_on_bidask_fop_v1_callback(fop_bidask_callback_v1)
if timestamp.time() < datetime.time(13, 25, 0):
    # 最後一盤 13:25:00 前下回補單。一率市價單回補
    self.order_cover = sdt.api.Order(
        price=0,
        quantity=current_number_to_cover,
        action="Buy",
        price_type="MKT",
        order_type="ROD",
        order_lot="Common",
        first_sell="false",
        account=sdt.api.stock_account,
    )
else:
    # 13:25 後回補,最後一盤,只能用:限價+漲停價格 來確保一定會補回來
    self.order_cover = sdt.api.Order(
        price=self.today_limit_up,
        quantity=current_number_to_cover,
        action="Buy",
        price_type="LMT",
        order_type="ROD",
        order_lot="Common",
        first_sell="false",
        account=sdt.api.stock_account,
    )

# 下限價單 或是 下市價單
if sdt.config[w.order_setting][w.order_limited] == w.Yes:
    self.order_put = sdt.api.Order(
        price=self.today_put_order_price,
        quantity=abs(units),
        action="Sell",
        price_type="LMT",
        order_type="ROD",
        order_lot="Common",
        first_sell="true",
        account=sdt.api.stock_account,
    )
else:
    self.order_put = sdt.api.Order(
        price=0,
        quantity=abs(units),
        action="Sell",
        price_type="MKT",
        order_type="ROD",
        order_lot="Common",
        first_sell="true",
        account=sdt.api.stock_account,
    )

self.trade_put = sdt.place_order(self.__contract__, self.order_put)

api.activate_ca 啟動電子憑證

官方說明文件: https://sinotrade.github.io/tutor/order/CA/ 在下單之前,需要先下載永豐證券帳戶的下單電子憑證,下載方式請參考官方說明 https://www.sinotrade.com.tw/CSCenter/CSCenter_13_1?tab=2 下載完成後,可以透過api.activate_ca來啟用下單電子憑證,範例如下:

from dotenv import load_dotenv
import os
import shioaji as sj

load_dotenv('D:\\python\\shioaji\\.env') #讀取.env中的環境變數
api = sj.Shioaji()
api.login(
    person_id=os.getenv('YOUR_PERSON_ID'), 
    passwd=os.getenv('YOUR_PASSWORD')
)

result = api.activate_ca(
    ca_path=os.getenv('YOUR_CA_PATH'), # 下單電子憑證路徑及檔案名稱
    ca_passwd=os.getenv('YOUR_CA_PASS'), # 下單電子憑證密碼
    person_id=os.getenv('YOUR_PERSON_ID'), # 身份證字號
)
print(result)

api.logout()

下單電子憑證及Stock股票Order建立

出處 : https://ithelp.ithome.com.tw/articles/10272506?sc=iThelpR

啟用下單電子憑證前要先執行api.login進行登入。在這裡一樣把電子憑證相關資訊先儲存在env檔案中,再透過os.getenv()取得資訊並傳入activate_ca中,若電子憑證啟用成功,則回傳的result就會是True。

若你要用虛擬帳戶登入並練習或測試下單功能,不必啟動電子憑證,可跳過這個步驟。

Order物件建立說明

官方說明文件:https://sinotrade.github.io/tutor/order/Stock/#making-order-object 在發送委託單前,要先產生一個Order物件。 Order物件建立的參數說明如下:

參數參數說明參數範例
price委託價格18.5
quantity委託數量1
action委託單動作{Buy, Sell}
price_type價格類型{LMT, MKT, MKP} (限價、市價、範圍市價)
order_type委託單類型{ROD, IOC, FOK} (當日有效、立即成交否則取消、全部成交否則取消)
order_cond委託單種類{Cash, MarginTrading, ShortSelling} (現股、融資、融券)
order_lot委託單交易單位{Common, Fixing, Odd, IntradayOdd} (整股、盤後定價、盤後零股、盤中零股)
first_sell是否為現沖先賣{true, false}
octype倉別{Auto, NewPosition, Cover, DayTrade} (自動、新倉、平倉、當沖)
OptionRight選擇權類別{Call, Put}
account交易帳戶可由API取得account物件

order_cond、order_lot及first_sell,為股票Order物件特有屬性 octype,為期貨或選擇權Order物件特有屬性 OptionRight為選擇權Order物件特有屬性

現股買進,Order範例

order = api.Order(
    price=12, 
    quantity=1, 
    action=sj.constant.Action.Buy, #買進
    price_type=sj.constant.StockPriceType.LMT, 
    order_type=sj.constant.TFTOrderType.ROD, 
    order_lot=sj.constant.TFTStockOrderLot.Common, 
    account=api.stock_account
)

現股賣出,Order範例

order = api.Order(
    price=12, 
    quantity=1, 
    action=sj.constant.Action.Sell, #賣出
    price_type=sj.constant.StockPriceType.LMT, 
    order_type=sj.constant.TFTOrderType.ROD, 
    order_lot=sj.constant.TFTStockOrderLot.Common, 
    account=api.stock_account
)

現沖先賣,Order範例

order = api.Order(
    price=12, 
    quantity=1, 
    action=sj.constant.Action.Sell,
    price_type=sj.constant.StockPriceType.LMT,
    order_type=sj.constant.TFTOrderType.ROD,
    order_lot=sj.constant.TFTStockOrderLot.Common,
    first_sell=sj.constant.StockFirstSell.Yes, #現沖先賣,設定為StockFirstSell.Yes or True
    account=api.stock_account
)

盤中零股,Order範例

order = api.Order(
    price=12, 
    quantity=1, 
    action=sj.constant.Action.Buy, #買進
    price_type=sj.constant.StockPriceType.LMT, 
    order_type=sj.constant.TFTOrderType.ROD, 
    order_lot=sj.constant.TFTStockOrderLot.IntradayOdd, #指定盤中零股
    account=api.stock_account
)

盤中零股,Order範例

order = api.Order(
    price=12, 
    quantity=1, 
    action=sj.constant.Action.Buy, #買進
    price_type=sj.constant.StockPriceType.LMT, 
    order_type=sj.constant.TFTOrderType.ROD, 
    order_lot=sj.constant.TFTStockOrderLot.IntradayOdd, #指定盤中零股
    account=api.stock_account
)

盤後定價,Order範例

order = api.Order(
    price=12, 
    quantity=1, 
    action=sj.constant.Action.Buy, #買進
    price_type=sj.constant.StockPriceType.LMT, 
    order_type=sj.constant.TFTOrderType.ROD, 
    order_lot=sj.constant.TFTStockOrderLot.Fixing, #指定盤後定價
    account=api.stock_account
)

盤後零股,Order範例

order = api.Order(
    price=12, 
    quantity=1, 
    action=sj.constant.Action.Buy, #買進
    price_type=sj.constant.StockPriceType.LMT, 
    order_type=sj.constant.TFTOrderType.ROD, 
    order_lot=sj.constant.TFTStockOrderLot.Odd, #指定盤後零股
    account=api.stock_account
)

以上為現股Order的建立及下單相關操作,若是要使用融資或融券,只要在建立Order時指定order_cond為StockOrderCond.MarginTrading(融資)或是StockOrderCond.ShortSelling(融券)即可;若在建立Order時沒有指定order_cond,預設都是以StockOrderCond.Cash建立。


from Jlab.watcher import Watcher
import shioaji as sj
import json
import os
import sys


def login(simulation=False):
    api = sj.Shioaji(simulation=simulation)
    token_file = os.environ["HOME"] + "/.mybin/shioaji_tokens.json"
    with open(token_file, "r") as f:
        users = json.load(f)
        print("All users: " + ", ".join(users.keys()))

        user = input("Select a user from the list above: ")
        if user not in users:
            print("User not found.")
            sys.exit()

        api.login(
            users[user]["api_key"], users[user]["secret_key"], fetch_contract=False
        )
        api.fetch_contracts(contract_download=True)
        print(f"Logged in as {user}")
        return api


def get_near_month_txf_contract(api):
    contract = min(
        [x for x in api.Contracts.Futures.TXF if x.code[-2:] not in ["R1", "R2"]],
        key=lambda x: x.delivery_date,
    )
    return contract


def simulation(api):
    contract = api.Contracts.Stocks.TSE["2890"]
    # order - edit it
    order = api.Order(
        action=sj.constant.Action.Buy,
        price=20,
        quantity=1,
        price_type=sj.constant.StockPriceType.LMT,
        order_type=sj.constant.OrderType.ROD,
        account=api.stock_account,
    )

    # place order
    trade = api.place_order(contract, order, timeout=0)
    print(trade)


if __name__ == "__main__":
    Watcher()
    # 設置參數以決定是使用模擬還是實際交易
    SIMULATION = False
    api = login(simulation=SIMULATION)
    if SIMULATION:
        simulation(api)
    else:
        contract = get_near_month_txf_contract(api)
        print(contract)

        # print(api.account_balance())
        # print(api.list_positions(api.stock_account))
        contracts = [api.Contracts.Stocks["2330"], api.Contracts.Stocks["2317"]]
        snapshots = api.snapshots(contracts)
        print(snapshots)
        # Stock default account  證券目前的預設帳戶
    print(api.stock_account)

    # Futures default account 期貨目前的預設帳戶
    print(api.futopt_account)

    api.logout()

accounts = api.list_accounts()

若你登入虛擬環境後,執行print(accounts),會顯示以下內容

[FutureAccount(person_id='QBCCAIGJBJ', broker_id='F002000', account_id='9100020', signed=True, username='PAPIUSER01'), StockAccount(person_id='QBCCAIGJBJ', broker_id='9A95', account_id='0504350', signed=True, username='PAPIUSER01')]

可以看到虛擬環境帳號底下,分別有FutureAccount期貨帳戶及StockAccount股票帳戶,相關變數說明如下:

變數名稱說明
person_id身份證號碼
broker_id券商分點號碼
account_id帳戶號碼
signed是否已簽署API下單若帳號資訊無此變數,表示此帳戶尚未簽署API下單
username使用者名稱若使用個人帳號登入,此欄位顯示你的姓名

若你的帳戶尚未簽署API下單,可開啟永豐金iLeader,找到「數位e櫃臺」並開啟

https://github.com/eyelash500/2021_ironman_Shioaji

import threading
import time
from datetime import datetime

import shioaji as sj


class trader:
    """The Shioaji Object"""

    def __init__(self) -> None:
        self.simulation = True  # 是否為測試環境
        self.id = "PAPIUSER07"
        self.pwd = "2222"
        self.api = sj.Shioaji()
        self.diff = 0  # 大臺的點數差

    def login(self, id=None, pwd=None, simulation=True):
        """Login to Shioaji.
        Args:
            id(str): user ID
            pwd(str): the login password
        Returns:
            bool: True is login successfully, False is not.
        """

        print(f"=start login-{datetime.now().strftime('%Y%m%d')}")
        if id and pwd:
            self.id = id
            self.pwd = pwd

        try:
            # 登入 shioaji
            self.api = sj.Shioaji(simulation=simulation)
            self.api.login(person_id=self.id, passwd=self.pwd)
        except Exception as exc:
            print(f"id={self.id}, pwd={self.pwd}...{exc}")
            return False

        return True

    def _get_subscribe(self) -> bool:
        """Get the subscibe format."""

        print(self.api.quote.subscribe)

        return True

    def subscribe(self, contract):
        """subscribe the contract quote."""

        print("=Subscribe=")
        self.api.quote.subscribe(contract, quote_type=sj.constant.QuoteType.Tick)

    def unsubscribe(self, contract):
        """unsubscribe the contract."""

        print("unsubscribe")
        self.api.quote.unsubscribe(contract, quote_type=sj.constant.QuoteType.Tick)

    def quote_callback(self, topic: str, quote: dict):
        """Get the quote info and change the oder price.
        The quote's format is v0: quote is a dict and the value is a list.
        """

        print(
            f"{topic}-Price:[{quote['Close']}]Diff:[{quote['DiffPrice']}]volumn:[{quote['Volume']}]"
        )

        if topic.find("TFE/TXF") > 0:
            self.diff = quote["DiffPrice"][0]
        elif topic.find("OPT/TX") > 0:
            reduced_point = 1

            # 設定要減少的點數
            if self.diff < quote["Close"][0]:
                reduced_point = self.diff  # 比市價還要低的數字
            else:
                # 當變動很多時,要剪去的價格會比較大,但比現價還要小
                reduced_point = quote["Close"][0] - reduced_point

            self.change_price(quote["Close"], True, reduced_point)  # 價格比現價還要低,

    def change_price(self, price, diff, points):
        """Simulate to change the price of the order."""

        self.mxf_price = price[0] - points if diff else price[0] + points
        print(f"選擇權:current price:{price[0]}-new price:{self.mxf_price}")


def sleeper():
    """For sleeping... Let us get the quote and change the price."""

    print("-start sleep...")
    time.sleep(60)
    print("-Wake up!!!!")


timer = threading.Thread(target=sleeper)  # 建立執行緒

t = trader()
t.login()

t.subscribe(t.api.Contracts.Futures.TXF["TXF202110"])  # 訂閱臺指期-2021/10
t.subscribe(t.api.Contracts.Options.TX2.TX2202110016300C)  # 訂閱臺指選擇權10W2月 16300C

t.api.quote.set_quote_callback(t.quote_callback)  # 設定處理回報的功能

timer.start()  # 執行thread
timer.join()  # 等待結束thread

t.unsubscribe(t.api.Contracts.Futures.TXF["TXF202110"])  # 取消訂閱臺指期-2021/10
t.unsubscribe(t.api.Contracts.Options.TX2.TX2202110016300C)  # 取消訂閱臺指選擇權10W2月 16300C
# 先透過 createPositionFromPnl 建立當下部位狀態,後面透過 Shioaji 成交回報 即時更新股票持倉部位

from loguru import logger
from dataclasses import dataclass
from typing import Optional, Dict, List
import math
import shioaji as sj
from shioaji.constant import OrderState, Action, StockOrderCond


@dataclass
class StockPosition:
    symbol: str
    action: Action
    quantity: int
    cost: int
    ordercond: StockOrderCond


class PositionAid:
    def __init__(self, api: sj.Shioaji):
        self.api = api
        self.api.set_order_callback(self.onOrderStatusChange)
        self.position: Dict[str, StockPosition] = {}

    def onOrderStatusChange(self, state: OrderState, data: Dict):
        if state == OrderState.TFTOrder:
            pass
        elif state == OrderState.TFTDeal:
            self.updatePosition(data)

    def createPositionFromPnl(self):
        """
        從 api list_position 損益建立 Position 資訊
        """
        all_pnl = self.api.list_positions()
        for pnl in all_pnl:
            position = StockPosition(
                symbol=pnl.code,
                action=pnl.direction,
                quantity=pnl.quantity,
                cost=math.floor(pnl.price * pnl.quantity * 1000),
                ordercond=pnl.cond,
            )
            self.position[position.symbol] = position

    def getAllPosition(self) -> List[StockPosition]:
        return list(self.position.values())

    def updatePosition(self, deal: Dict):
        code = deal["code"]
        action = deal["action"]
        order_cond = deal["order_cond"]
        quantity = deal["quantity"]
        cost = math.floor(deal["price"] * deal["quantity"] * 1000)

        position = self.getPosition(code)
        if position == None:
            position = StockPosition(
                symbol=code,
                action=action,
                quantity=quantity,
                cost=cost,
                ordercond=order_cond,
            )
        else:
            if position.action == action:
                position.quantity += quantity
                position.cost += cost
            else:
                position.quantity -= quantity
                position.cost -= cost
        self.position[code] = position
        logger.info(
            f"{code} {self.api.Contracts.Stocks[code].name} {action} {deal['price']} 元 {quantity}張  -> {position}"
        )

    def getPosition(self, code: str) -> Optional[StockPosition]:
        """code: 股票代碼
        透過 股票代碼 取得 StockPosition 資訊
        沒有此檔股票 則回傳 = None
        """
        return self.position.get(code, None)


if __name__ == "__main__":
    # 建立 Shioaji 並登入
    api = sj.Shioaji() 
    api.login("SJ_USER","SJ_PASSWORD")

    # 建立 PositionAid
    aid = PositionAid(api) # 自動接手 SJ 主動回報 並處理 成交資訊
    aid.createPositionFromPnl() # 從 api list_position 損益建立 Position 資訊
    
    aid.getPosition("2330") # 取得 2330 持倉資訊 (如果沒有 2330 則得到 None)

CancelAllOrder.py

# 刪除全部的委託單
api.update_status()
for idx,t in enumerate(api.list_trades()):
    if t.status.status in [shioaji.constant.Status.PreSubmitted,shioaji.constant.Status.Submitted,shioaji.constant.Status.PartFilled] :
        api.cancel_order(t,timeout=0)

clear_all.py

# 13:25之後用漲跌停價格反向出場
def clear_all():
    """13:25之後用漲跌停價格反向出場"""
    #只處理今天新增的現股 (現股當沖、不含興櫃)
    pnls = [one for one in api.list_positions() if abs(one.quantity) - one.yd_quantity!=0]
    for one_pnl in pnls:
        contract = api.Contracts.Stocks[one_pnl.code]
        if contract == None:
            print(f"無此商品 {one_pnl.code}")
            continue
        action = "Buy"
        price = contract.limit_up
        if one_pnl.direction=='Buy':
            action = "Sell"
            price = contract.limit_down
        quantity = abs(one_pnl.quantity) - one_pnl.yd_quantity
        if quantity > 0 and contract.exchange!="OES":
            order = api.Order(price=price, quantity=quantity, action=action, price_type="LMT", order_type="ROD", order_lot="Common",first_sell="false")
            for _ in range(0,quantity // 499):
                order.quantity = 499
                api.place_order(api.Contracts.Stocks[one_pnl.code],order)
                print(f"{one_pnl.code} {[pnl.pnl for pnl in pnls if pnl.code==one_pnl.code]} {order.action.value} {order.quantity} 張 {order.price} 元")
            left = quantity % 499
            if left > 0:
                order.quantity = left
                api.place_order(api.Contracts.Stocks[one_pnl.code],order)
                print(f"{one_pnl.code} {[pnl.pnl for pnl in pnls if pnl.code==one_pnl.code]} {order.action.value} {order.quantity} 張 {order.price} 元")

永豐金API之30天不中斷Q&A 系列

創立每列

import pandas as pd

# 創建空的 DataFrame
df = pd.DataFrame(columns=['Technology', 'Consumer', 'Healthcare', 'Energy'])

# 股票字典
stocks = {
    'GOOG': ['Technology', 'Healthcare'],
    'AAPL': ['Consumer']
}

# 遍歷股票字典並將 DataFrame 中相應的單元格設置為 True
for stock, industries in stocks.items():
    for industry in industries:
        df.loc[stock, industry] = True

# 將缺失值(即 False)替換為 False
df.fillna(False, inplace=True)

print(df)

抓取美股分K歷史數據

import yfinance as yf
import pandas as pd
import datetime as dt

# 設置股票代碼和時間範圍
ticker = 'AAPL'
end_date = dt.datetime.now()
start_date = end_date - dt.timedelta(days=30)

# 將時間範圍拆分成多個時間段,每個時間段為 7 天
date_ranges = pd.date_range(start=start_date, end=end_date, freq='7d')

# 獲取歷史數據
dataframes = []
for i in range(len(date_ranges) - 1):
    start = date_ranges[i].strftime('%Y-%m-%d')
    end = date_ranges[i+1].strftime('%Y-%m-%d')
    df = yf.download(ticker, start=start, end=end, interval='1m')
    dataframes.append(df)

# 合併為一個 DataFrame 對象
data = pd.concat(dataframes)

# 打印 DataFrame 對象
print(data)

keyvalue-sqlite

# pip install keyvalue-sqlite
from keyvalue_sqlite import KeyValueSqlite

DB_PATH = './db.sqlite'

db = KeyValueSqlite(DB_PATH, 'table-name')

# Now use standard dictionary operators
db.set('0', {"1101":23, "2330": 100})
actual_value = db.get('0')
print(actual_value)
db.set('0', '211')
actual_value = db.get('0')
print(actual_value)
db.remove('0')
actual_value = db.get('0')
print(actual_value)

sched 定時

import sched
import time
import datetime

'''
導入 sched 模塊和 time 模塊。
定義一個 main() 函數,用於執行程序的主要邏輯。
定義一個 run_main() 函數,用於在指定時間執行 main() 函數。
獲取當前時間,並計算距離下一個執行時間的時間差 delta。
使用 sched 模塊的 enter() 方法,將 run_main() 函數添加到調度隊列中,並設置下一次執行的時間為當前時間加上 delta。
使用 sched 模塊的 run() 方法,啟動調度器。
'''

def main():
    # 在這裡編寫程序的主要邏輯
    print("Hello, world!")

def run_main():
    # 獲取當前時間
    now = datetime.datetime.now()

    # 計算距離下一個執行時間的時間差
    next_time = now.replace(hour=8, minute=45, second=0, microsecond=0)
    if next_time < now:
        next_time += datetime.timedelta(days=1)
    delta = next_time - now

    # 計算下一個執行時間,並輸出日誌
    next_time_str = next_time.strftime("%Y-%m-%d %H:%M:%S")
    print(f"Next run time: {next_time_str}")

    # 在指定時間執行程序的主要邏輯
    scheduler = sched.scheduler(time.time, time.sleep)
    scheduler.enter(delta.total_seconds(), 1, main, ())
    scheduler.run()

if __name__ == "__main__":
    run_main()

pdf 分割

from PyPDF2 import PdfReader, PdfWriter


# PDF文件分割
def split_pdf(read_file, out_detail):
    try:
        fp_read_file = open(read_file, "rb")
        pdf_input = PdfReader(fp_read_file)  # 將要分割的PDF內容格式話
        page_count = pdf_input.pages  # 獲取PDF頁數
        print(page_count)  # 打印頁數

        with open(out_detail, "r", True, "utf-8") as fp:
            # print(fp)
            txt = fp.readlines()
            # print(txt)
            for detail in txt:  # 打開分割標準文件
                # print(type(detail))
                pages = detail.strip()  # 空格分組
                #  write_file, write_ext = os.path.splitext(write_file)  # 用於返回文件名和擴展名元組
                pdf_file = f"{pages}.pdf"
                # liststr=list(map(int, pages.split('-')))
                # print(type(liststr))
                start_page, end_page = list(map(int, pages.split("-")))  # 將字符串數組轉換成整形數組
                start_page -= 1
                try:
                    print(f"開始分割{start_page}頁-{end_page}頁,保存為{pdf_file}......")
                    pdf_output = PdfWriter()  # 實例一個 PDF文件編寫器
                    for i in range(start_page, end_page):
                        pdf_output.add_page(pdf_input.pages[i])
                    with open(pdf_file, "wb") as sub_fp:
                        pdf_output.write(sub_fp)
                    print(f"完成分割{start_page}頁-{end_page}頁,保存為{pdf_file}!")
                except IndexError:
                    print(f"分割頁數超過了PDF的頁數")
        # fp.close()
    except Exception as e:
        print(e)
    finally:
        fp_read_file.close()


split_pdf("./The Art of Writing Efficient Programs An advanced programmers guide to efficient hardware utilization and compiler... (Fedor G. Pikus) (Z-Library).pdf", "config.txt")

使用 urllib.parse 模組的 unquote() 函數將編碼過的 URL 字符串解碼

import urllib.parse

url = "https://www.finlab.tw/wp-content/uploads/2022/07/%E6%88%AA%E5%9C%96-2022-07-25-%E4%B8%8B%E5%8D%8812.42.21-1536x431.png"
decoded_url = urllib.parse.unquote(url)

print(decoded_url)

dataFrame 寫到csv, 再從csv 讀回dataframe

import pandas as pd

# Create a sample DataFrame
data = {'Name': ['Alice', 'Bob', 'Charlie'], 
        'Age': [25, 30, 35], 
        'City': ['New York', 'Paris', 'London']}
df = pd.DataFrame(data)

# Write the DataFrame to a CSV file
df.to_csv('data.csv', index=False)

# Read the CSV file back into a DataFrame
new_df = pd.read_csv('data.csv')

# Print the original and new DataFrames
print('Original DataFrame:\n', df)
print('\nNew DataFrame:\n', new_df)

顯示 datafrmae index 跟 欄位型態

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'col1': [1, 2, 3],
    'col2': ['a', 'b', 'c']
}, index=pd.date_range('2022-01-01', periods=3))

print(df.index)
print(df.dtypes)

已經在 sqlite 的 primary_keys 不 insert data

import pandas as pd
import sqlite3


class FinmindAPI:
    db_path = "example.db"

    @staticmethod
    def insert_db(data):
        conn = sqlite3.connect(FinmindAPI.db_path)
        create_table = """
    CREATE TABLE IF NOT EXISTS TaiwanStockMonthRevenue (
        stock_id TEXT,
        date TEXT,
        revenue INTEGER
    );"""

        conn.execute(create_table)
        # Check if data already exists in the database
        primary_keys = ["stock_id", "date"]
        existing_data = pd.read_sql_query(
            f"SELECT {', '.join(primary_keys)} FROM TaiwanStockMonthRevenue", con=conn
        )
        if (
            existing_data.set_index(primary_keys)
            .index.isin(data.set_index(primary_keys).index)
            .any()
        ):
            print("Data already exists in database, skipping insertion")
            conn.close()
            return

        # Insert data into database
        data.to_sql(
            name="TaiwanStockMonthRevenue", con=conn, if_exists="append", index=False
        )
        conn.close()
        print(f"Data inserted to database successfully.")


if __name__ == "__main__":
    # Create example data
    data1 = pd.DataFrame(
        {
            "stock_id": ["2330", "2454", "2382"],
            "date": ["2020-01", "2020-01", "2020-01"],
            "revenue": [1000, 2000, 3000],
        }
    )
    data2 = pd.DataFrame(
        {
            "stock_id": ["2330", "2454", "2382"],
            "date": ["2020-02", "2020-02", "2020-02"],
            "revenue": [4000, 5000, 6000],
        }
    )

    # Insert first set of data
    print("Inserting first set of data ...")
    FinmindAPI.insert_db(data1)
    # Insert same data again
    print("Inserting same data again ...")
    FinmindAPI.insert_db(data1)
    # Insert new set of data
    print("Inserting new set of data ...")
    FinmindAPI.insert_db(data2)

產生num組加總為1的小數數值

import random

def generate_random_decimals(num):
    """
    產生num組加總為1的小數數值

    Args:
        num: 需要產生的小數數值的組數

    Returns:
        一個包含num組小數數值的列表,每組小數數值都是一個長度為3的列表
    """
    result = []
    # 初始化三個數值
    for i in range(num):
        data = [0.0] * 3
        # 隨機產生兩個小數數值
        data[0] = round(random.uniform(0, 1), 2)
        data[1] = round(random.uniform(0, 1 - data[0]), 2)
        # 計算第三個小數數值
        data[2] = round(1 - data[0] - data[1], 2)
        result.append(data)
    return result

if __name__ == '__main__':
    data = generate_random_decimals(2)
    print(data)

讀檔正則取圖下載

import os
import re
import requests
import cv2
import numpy as np

# 設置要讀取的文件路徑
file_path = "./learn_network.md"

# 設置下載目標目錄
target_dir = "images"

# 設置轉換後的圖檔格式
target_ext = ".jpg"

# 創建目標目錄
if not os.path.exists(target_dir):
    os.makedirs(target_dir)

# 定義正則表達式來匹配圖像URL
with open(file_path, "r") as f:
    text = f.read()
    pattern = r"\!\[.*?\]\((.*?)\)"
    image_urls = re.findall(pattern, text)

    for img in image_urls:
        print(img)
    input()

    for url in image_urls:
        try:
            print(url)
            response = requests.get(url, stream=True)
            if response.status_code == 200:
                # 取得圖像的文件名和擴展名
                filename = url.split("/")[-1]
                ext = os.path.splitext(filename)[1].lower()
                img_array = np.asarray(bytearray(response.content), dtype=np.uint8)
                img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
                filename = os.path.splitext(filename)[0] + target_ext
                filepath = os.path.join(target_dir, filename)
                cv2.imwrite(filepath, img)
                print(filename + " 下載成功")
            else:
                print(filename + " 下載失敗")
        except Exception as e:
            print(f"下載圖像失敗: {e}")

函數插入log

import os
import sys

def insert_to_func(lines, func_lines, file_name):
    for i in func_lines:
        for j in range(i-1, len(lines)):
            if str(lines[j]).find(';') != -1:
                break

            if str(lines[j]).find('{') != -1:
                if str(lines[j]).find('}') != -1:
                    break

                if str(file_name).find('.java') != -1:
                    lines.insert(j+1, '\tSystem.out.println("YAO [" + Thread.currentThread().getStackTrace()[2].getClassName() + "|" + Thread.currentThread().getStackTrace()[2].getMethodName() + "|" + Thread.currentThread().getStackTrace()[2].getFileName() + ":" + Thread.currentThread().getStackTrace()[2].getLineNumber()+"]");\n')
                elif str(file_name).find('.cpp') != -1 or str(file_name).find('.c') != -1 or str(file_name).find('.cc') != -1:
                    lines.insert(j+1, '::printf ("This is line %d of file %s (function %s)\\n", __LINE__, __FILE__, __func__);')
                elif str(file_name).find('.go'): 
                    lines.insert(j+1,'\tutils.Trace("")')

                break

    return lines

def main():
    if len(sys.argv) < 2:
        print("please input python test.py filename")
        return

    file_name = sys.argv[1]
    print(file_name)
 
    if str(file_name).find('.java') != -1:
        os.system("ctags-exuberant -x " + file_name + " | ack -o -w 'method\s+.*' | ack -o '\d+\s+.*' | ack -o '^\d+\s+' | sort -k 1 -nr > /tmp/test.txt")
    elif str(file_name).find('.cpp') != -1 or str(file_name).find('.c') != -1:
        os.system("ctags-exuberant -x " + file_name + " | ack -o -w 'function\s+.*' | ack -o '\d+\s+.*' | ack -o '^\d+\s+' | sort -k 1 -nr > /tmp/test.txt")
    elif str(file_name).find('.go') != -1:
        os.system("ctags-exuberant -x " + file_name + " | ack -o -w 'func.*'  | ack -o '\d+\s+.*' | ack -o '^\d+\s+' | sort -k 1 -nr > /tmp/test.txt")
    else:
        print('unknown file type')
        return

    with open('/tmp/test.txt', 'r+') as f:
        func_lines = [int(i) for i in f.read().splitlines()]

    with open(file_name, 'r+') as f:
        lines = f.read().splitlines()

    insert_list_finish = insert_to_func(lines, func_lines, file_name)

    with open(file_name, "w+") as new_file:
        for l in insert_list_finish:
            new_file.write(l + '\n')

if __name__=='__main__':
    main()

輸入日期取得上週五日期

from datetime import date, datetime, timedelta

#today = date.today()  # 取得今天日期
today = datetime(2023, 3, 10)
last_friday = today - timedelta(days=today.weekday() + 3)  # 回推到上週五
last_friday_str = last_friday.strftime('%Y/%m/%d')  # 轉換成指定格式的字串

print(last_friday_str)  # 印出上週五的日期

模擬程式執行一段時間後出現問題需要重啟

import os
import sys
import time
import threading

def main():
    while True:
        # 模擬程式執行一段時間後出現問題需要重啟
        time.sleep(5)
        if should_restart():
            restart_program()

def should_restart():
    try:
        a = 1 / 0
        return False
    except Exception as err:
        print(err)
        return True

def restart_program():
    python = sys.executable
    print("Restarting program with PID {} and TID {}".format(os.getpid(), threading.get_ident()))
    os.execl(python, python, *sys.argv)

if __name__ == '__main__':
    print("Starting program with PID {} and TID {}".format(os.getpid(), threading.get_ident()))
    main()

輸入日期取得上個月最後一天日期

import datetime

def get_last_day_of_month(date):
    first_day = datetime.datetime(date.year, date.month, 1)  # 當月份的第一天
    last_day = first_day.replace(month=first_day.month+1, day=1) - datetime.timedelta(days=1)  # 當月份的最後一天
    return last_day.strftime('%Y-%m-%d')  # 格式化輸出日期

# 呼叫函數並輸出結果
date = datetime.datetime(2023, 3, 12)  # 要計算的日期
print(get_last_day_of_month(date))

找出 Pct_Change_12M 和 Volume_12M 都為True的月份

import pandas as pd
import yfinance as yf
from datetime import timedelta

# 下載資料
# df = yf.download("2324.TW", start="2000-01-01", end="2023-01-01")
df = yf.download("8299.TWO", start="2000-01-01", end="2023-01-01")

# 計算月K資料
monthly_df = df.resample("M").apply(
    {"Open": "first", "High": "max", "Low": "min", "Close": "last", "Volume": "sum"}
)

# 將日期轉換為月初的日期
monthly_df.index = monthly_df.index.to_period("M").to_timestamp("M")

# 計算月漲幅
monthly_df["Pct_Change"] = monthly_df["Close"].pct_change() * 100

# 計算成交量12個月移動平均
monthly_df["Volume_MA12"] = monthly_df["Volume"].rolling(window=12).mean()

# 計算是否股價創下過去12個月新高
monthly_df["New_High_12M"] = (
    monthly_df["High"] == monthly_df["High"].rolling(window=12).max()
)

# 判斷當月漲幅、成交量和股價是否都超過過去12個月移動平均和最高價格
monthly_df["Pct_Change_12M"] = (
    monthly_df["Pct_Change"] > monthly_df["Pct_Change"].rolling(window=12).mean()
)
monthly_df["Volume_12M"] = monthly_df["Volume"] > monthly_df["Volume_MA12"]
monthly_df["All_12M"] = (
    monthly_df["Pct_Change_12M"] & monthly_df["Volume_12M"] & monthly_df["New_High_12M"]
)

# 找出 Pct_Change_12M、Volume_12M、New_High_12M 和 within_3_months 都為 True 的月份
entries = monthly_df.loc[monthly_df["All_12M"]]


# 將 index 日期加 3 個月,並命名為 'Next_3_Months'
entries["Next_3_Months"] = entries.index + pd.DateOffset(months=3)

# 往下 shift 一列
entries["Next_3_Months"] = entries["Next_3_Months"].shift(1)

entries["Within_3_Months"] = entries["Next_3_Months"] >= entries.index


print(entries)

永豐期貨/選擇權計算周選代號

from multiprocessing import Process, Queue
from shioaji.contracts import Contract
from shioaji import Exchange
from time import sleep
from line_notify import LineNotify
from datetime import datetime, timedelta

import re
import redis
import sys
import platform
import signal
import shioaji as sj
import os
import json


def get_option_week(api):
    def get_previous_wednesday():
        today = datetime.today()
        wednesday = today - timedelta(days=today.weekday()) + timedelta(days=2)
        previous_wednesday = wednesday - timedelta(days=7)
        return previous_wednesday.date()

    def get_this_week_wednesday():
        today = datetime.today()
        wednesday = (today + timedelta(days=(2 - today.weekday()))).date()
        return wednesday

    def get_next_week_wednesday():
        today = datetime.today()
        wednesday = (today + timedelta(days=(2 - today.weekday() + 7))).date()
        return wednesday

    option_symbols = (str(api.Contracts.Options))[1:-1].split(", ")
    near_week_option_symbol = [string for string in option_symbols if "TX" in string]
    print(near_week_option_symbol)

    symbols = sorted([x.symbol for x in api.Contracts.Options.TXO])
    near_option_symbol_date = symbols[0][3:9]

    for option in api.Contracts.Options:
        for contract in option:
            if "TX" in contract.category and near_option_symbol_date in contract.symbol:
                now = datetime.now()
                wednesday_time = get_this_week_wednesday()
                wednesday_time = datetime.combine(
                    wednesday_time, datetime.min.time()
                ) + timedelta(hours=15)

                # 根據當前時間判斷是否在星期三 15:00之前。如果在此時間之前,則列印上週三和本週三的日期;否則列印本週三和下週三的日期:
                if now < wednesday_time:
                    if datetime.strptime(
                        contract.update_date, "%Y/%m/%d"
                    ).date() >= get_previous_wednesday() and contract.delivery_date == get_this_week_wednesday().strftime(
                        "%Y/%m/%d"
                    ):
                        print(
                            contract.symbol,
                            contract.name,
                            contract.update_date,
                            contract.delivery_date,
                        )
                        return contract.symbol
                else:
                    if datetime.strptime(
                        contract.update_date, "%Y/%m/%d"
                    ).date() >= get_this_week_wednesday() and contract.delivery_date == get_next_week_wednesday().strftime(
                        "%Y/%m/%d"
                    ):
                        print(
                            contract.symbol,
                            contract.name,
                            contract.update_date,
                            contract.delivery_date,
                        )
                        return contract.symbol


with open(os.environ["HOME"] + "/.mybin/shioaji_token.txt", "r") as f:
    api = sj.Shioaji()
    kw_login = json.loads(f.read())
    api.login(**kw_login, contracts_timeout=300000)

    print(get_option_week(api))

    S = get_option_week(api)[:3]

    for option in api.Contracts.Options[S]:
        print(option)

import datetime

def get_option_week():
    # 設定選擇權到期時間
    expiration_time = datetime.time(15, 0, 0)

    # 取得當前日期的年份和月份
    today = datetime.date.today()
    year = today.year
    month = today.month

    # 找到本月的第一個星期三
    first_wednesday = datetime.date(year, month, 1)
    while first_wednesday.weekday() != 2:
        first_wednesday = first_wednesday.replace(day=first_wednesday.day+1)

    # 計算今天是第幾週
    today_year, today_week, _ = today.isocalendar()
    first_wednesday_year, first_wednesday_week, _ = first_wednesday.isocalendar()
    week_num = today_week - first_wednesday_week + 1

    # 判斷是否過了選擇權到期時間
    if datetime.datetime.now().time() >= expiration_time:
        week_num += 1

    return week_num
import cv2
import glob
import os

# 變更到指定尺寸,長寬邊不足者補黑色
def process_image(img, min_side=608):
    size = img.shape
    h, w = size[0], size[1]
    scale = max(w, h) / float(min_side)
    new_w, new_h = int(w / scale), int(h / scale)
    resize_img = cv2.resize(img, (new_w, new_h), cv2.INTER_AREA)  # 變更尺寸
    if new_w % 2 != 0 and new_h % 2 == 0:
        top, bottom, left, right = (
            (min_side - new_h) // 2,
            (min_side - new_h) // 2,
            (min_side - new_w) // 2 + 1,
            (min_side - new_w) // 2,
        )
    elif new_h % 2 != 0 and new_w % 2 == 0:
        top, bottom, left, right = (
            (min_side - new_h) // 2 + 1,
            (min_side - new_h) // 2,
            (min_side - new_w) // 2,
            (min_side - new_w) // 2,
        )
    elif new_h % 2 == 0 and new_w % 2 == 0:
        top, bottom, left, right = (
            (min_side - new_h) // 2,
            (min_side - new_h) // 2,
            (min_side - new_w) // 2,
            (min_side - new_w) // 2,
        )
    else:
        top, bottom, left, right = (
            (min_side - new_h) // 2 + 1,
            (min_side - new_h) // 2,
            (min_side - new_w) // 2 + 1,
            (min_side - new_w) // 2,
        )
    pad_img = cv2.copyMakeBorder(
        resize_img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=[0, 0, 0]
    )
    return pad_img


# 讀寫目錄
inputPath = "video"
outputPath = "images"

files = os.path.join(inputPath, "*.mp4")
files_grabbed = []
files_grabbed.extend(sorted(glob.iglob(files)))

for videoId in range(len(files_grabbed)):
    print(files_grabbed[videoId])
    raw = cv2.VideoCapture(files_grabbed[videoId])
    fIndex = 1
    fCount = 0

    while 1:
        # 影片轉圖片
        ret, frame = raw.read()
        fCount += 1
        if ret == True:
            if (fCount % 5) == 0:
                img_pad = process_image(frame, min_side=608)
                cv2.imwrite(
                    "%s/%02d-frame-608x608-%04d.jpg" % (outputPath, videoId, fIndex),
                    img_pad,
                )
                fIndex += 1
        else:
            break

目前主機時區跟 Taipei 時區差多少 offset?

使用 Python 標準庫的 datetime 模組的 now 方法,並使用 pytz 模組的 timezone 方法,得到目前的本地時間,然後再使用 astimezone 方法,轉換為 Taipei 時間,並通過計算得到 offset:

from datetime import datetime
import pytz

local_time = datetime.now()
local_time = pytz.timezone('UTC').localize(local_time)
taipei_time = local_time.astimezone(pytz.timezone('Asia/Taipei'))
offset = int((taipei_time - local_time).total_seconds() / 3600)
print(f"Local time is {local_time}. Taipei time is {taipei_time}. Offset is {offset} hours.")

裝飾器傳遞參數 *args 和 **kwargs

def funA(fn):
    # 定義一個嵌套函數
    def say(*args,**kwargs):
        print(args, kwargs)
        fn(*args,**kwargs)
    return say
@funA
def funB(arc):
    print("C語言中文網:",arc)

@funA
def other_funB(name,arc):
    print(name,arc)

funB("http://c.biancheng.net")
other_funB("Python教程:","http://c.biancheng.net/python")

【Python line_profiler & memory_profiler】分析每一行程式碼的耗時及記憶體佔用情況

https://codeantenna.com/a/XoAFyhqZ2B

pip install line_profiler
from line_profiler import LineProfiler


def func_line_time(follow=[]):
    def decorate(func):
        @wraps(func)
        def profiled_func(*args, **kwargs):
            try:
                profiler = LineProfiler()
                profiler.add_function(func)  # 增加每列的行數
                for f in follow:
                    profiler.add_function(f)
                profiler.enable_by_count()  # enable_by_count進行執行以獲取消耗的時間
                return func(*args, **kwargs)
            finally:
                profiler.print_stats()  # 顯示結果

        return profiled_func

    return decorate


@func_line_time()
def process(self, params):
    import pandas as pd`在這裡插入程式碼片`
    pass
  • Hit:程式碼運行次數;
  • %Time:程式碼佔了它所在函數的消耗的時間百分比,通常直接看這一列。
  • 在這裡我們主要觀察%Time 所佔用的百分比,對百分比較高的行數進行最佳化為第一選擇。

藉助記憶體分析庫 memory_profiler 查看每一行消耗了多少記憶體?

pip install memory_profiler
# 2. 藉助記憶體分析庫 memory_profiler 查看每一行消耗了多少記憶體?
from memory_profiler import profile


# precision 精確到小數點後幾位
# stream 此模組分析結果保存到‘memory_profiler.log’ 記錄檔。如果沒有此參數,分析結果會在控制檯輸出
# @profile(precision=4, stream=open('memory_profiler.log', 'w+'))
@profile(precision=4)
def process():
    print('memory analysis------------')
    pass

process()
pri

Mem 是總消耗的記憶體

  • Increment 是第幾行程式碼運行完後增加的記憶體
  • 通過memory_profiler 我們可以分析到每一行運行完後佔用的記憶體。這部分記憶體在處理程序沒結束的時候是不好被回收掉的,因此在這裡如果有哪一行邏輯運行一直在增加記憶體消耗,則這行可能是罪魁禍首。

simple memory_profiler example

from memory_profiler import profile

@profile
def my_func():
    a = [1] * (10 ** 6)
    b = [2] * (2 * 10 ** 7)
    del b
    return a

if __name__ == '__main__':
    my_func()

python 引用放掉記憶體

from memory_profiler import profile
import time
import gc

'''
在上面的程式碼中,我們創建了兩個類 A 和 B,並使用這些類創建了一個實例。由於類 B 引用了類 A 的實例,因此類 A 的實例將不能被內存管理器自動回收。因此,這將導致無法釋放內存的情況。
'''
# Example of un-collectable memory in Python
class A:
    def __init__(self, data):
        self.data = data
        self.b = B(self)

class B:
    def __init__(self, a):
        self.large_memory = bytearray(1024 * 1024)
        self.a = a

 
@profile
def call():
    a = A(10)
    del a
 
def main():
    while True:
        time.sleep(1)
        call()
 
if __name__ == "__main__":
    main()

Test memory leak

from memory_profiler import profile
import datetime as dt
import requests
import time
import httpx
import gc
 
@profile
def call():
    url = "https://api.bitopro.com/v3/provisioning/trading-pairs"
    response = httpx.get(url)
    if response.status_code == 200:
        data = response.json()
        print(response.status_code, dt.datetime.now())
        response.close()
        gc.collect()
    else:
        print("Request failed with status code:", response.status_code)
    
 
def main():
    while True:
        time.sleep(1)
        call()
 
if __name__ == "__main__":
    main()

每5秒委託成交統計

from datetime import timedelta
import time
import polars as pl
import datetime as dt 
import requests
import json


def get_webmsg(year, month, day):
    print(year, month, day)
    date = str(year) + "{0:0=2d}".format(month) + "{0:0=2d}".format(day)
    url_twse = (
        "http://www.twse.com.tw/exchangeReport/MI_5MINS?response=json&date=" + date
    )
    print(url_twse)
    res = requests.post(url_twse)
    data_json = json.loads(res.text)
    if data_json != {'stat': '很抱歉,沒有符合條件的資料!'}:
        df = pl.DataFrame(
            data_json["data"],
            schema=[
                "時間",
                "累積委託買進筆數",
                "累積委託買進數量",
                "累積委託賣出筆數",
                "累積委託賣出數量",
                "累積成交筆數",
                "累積成交數量",
                "累積成交金額",
            ],
        )
        return df
    return pl.DataFrame()


if __name__ == '__main__':
    start_date = dt.datetime(2019, 1, 1) #.strftime("%Y-%m-%d")
    end_date = dt.datetime(2023, 1, 20) #.strftime("%Y-%m-%d")

    while start_date < end_date:
        time.sleep(1)
        print(get_webmsg(start_date.year, start_date.month, start_date.day))
        start_date = start_date + timedelta(days=1)

polars

https://github.com/pola-rs/polars

pip install polars
import polars as pl

df = pl.DataFrame(
    {
        "A": [1, 2, 3, 4, 5],
        "fruits": ["banana", "banana", "apple", "apple", "banana"],
        "B": [5, 4, 3, 2, 1],
        "cars": ["beetle", "audi", "beetle", "beetle", "beetle"],
    }
)

print(df)
df = df.sort("fruits").select(
    [
        "fruits",
        "cars",
        pl.lit("fruits").alias("literal_string_fruits"),
        pl.col("B").filter(pl.col("cars") == "beetle").sum(),
        pl.col("A").filter(pl.col("B") > 2).sum().over("cars").alias("sum_A_by_cars"),
        pl.col("A").sum().over("fruits").alias("sum_A_by_fruits"),
        pl.col("A").reverse().over("fruits").alias("rev_A_by_fruits"),
        pl.col("A").sort_by("B").over("fruits").alias("sort_A_by_B_by_fruits"),
    ]
)
print(df)

multiprocessing queue with non blocking

from loguru import logger
import multiprocessing
import time


def processFun(conn):
    while True:
        try:
            print(conn.get(timeout=5)) # 等5秒沒數據就丟異常
        except Exception as e:
            logger.exception(e)
        print("接收到數據了", conn.qsize())


if __name__ == "__main__":
    # 創建管道
    conn = multiprocessing.Queue(10)
    # 創建子進程
    process = multiprocessing.Process(target=processFun, args=(conn,))
    # 啟動子進程
    process.start()
    i = 0
    while True:
        time.sleep(6)
        print(i)
        conn.put(i)
        i += 1

Dispatching Multiple WebSocketApps - Long-lived Connection

# import websocket, rel
#
# addr = "wss://api.gemini.com/v1/marketdata/%s"
# for symbol in ["BTCUSD", "ETHUSD", "ETHBTC"]:
#    ws = websocket.WebSocketApp(addr % (symbol,), on_message=lambda w, m : print(m))
#    ws.run_forever(dispatcher=rel, reconnect=3)
#
#
# rel.signal(2, rel.abort) # Keyboard Interrupt
# rel.dispatch()

from multiprocessing import Process
import time
import websocket
import rel
import signal
import os


def on_message(ws, message):
    print(message)


def on_error(ws, error):
    print(error)


def on_close(ws, close_status_code, close_msg):
    print("### closed ###")


def on_open(ws):
    print("Opened connection")


def receive_signal(signum, stack):
    print("Received:", signum, os.getpid())
    ws.close()
    rel.abort()
    ws.run_forever(dispatcher=rel, reconnect=5)  # Set dispatcher to automatic␣
    rel.signal(2, rel.abort)  # Keyboard Interrupt
    rel.dispatch()


def monitor(pid, ws):
    print("Waiting ...")
    time.sleep(3)
    os.kill(pid, signal.SIGUSR1)


if __name__ == "__main__":
    print(os.getpid())
    # websocket.enableTrace(True)
    ws = websocket.WebSocketApp(
        "wss://api.gemini.com/v1/marketdata/BTCUSD",
        on_open=on_open,
        on_message=on_message,
        on_error=on_error,
        on_close=on_close,
    )
    signal.signal(signal.SIGUSR1, receive_signal)
    monitor_task = Process(target=monitor, args=(os.getpid(), ws),)

    monitor_task.start()
    ws.run_forever(dispatcher=rel, reconnect=5)  # Set dispatcher to automatic␣
    rel.signal(2, rel.abort)  # Keyboard Interrupt
    rel.dispatch()
    monitor_task.join()

How can I send a signal from a python program?

import signal
import os
import time

def receive_signal(signum, stack):
    print('Received:', signum)

signal.signal(signal.SIGUSR1, receive_signal)
signal.signal(signal.SIGUSR2, receive_signal)

print('My PID is:', os.getpid())

while True:
    print('Waiting...')
    time.sleep(3)
    os.kill(os.getpid(), signal.SIGUSR1) 

記憶體監控

from loguru import logger
import os
import time
import platform
import psutil
import signal
import sys


class Watcher:
    def __init__(self):
        self.child = os.fork()
        if self.child == 0:
            return
        else:
            self.watch()

    def watch(self):
        try:
            os.wait()
        except KeyboardInterrupt:
            self.kill()
        sys.exit()

    def kill(self):
        try:
            print("kill")
            os.kill(self.child, signal.SIGKILL)
        except OSError:
            pass


logger.add(
    f"{__file__}.log",
    encoding="utf-8",
    enqueue=True,
    retention="10 days",
    # filter=error_only,
)


def getListOfProcessSortedByMemory():
    """
    Get list of running process sorted by Memory Usage
    """
    listOfProcObjects = []
    # Iterate over the list
    for proc in psutil.process_iter():
        try:
            # Fetch process details as dict
            pinfo = proc.as_dict(attrs=["pid", "name", "username"])
            pinfo["vms"] = proc.memory_info().vms / (1024 * 1024)
            # Append dict to list
            listOfProcObjects.append(pinfo)
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            pass
    # Sort list of dict by key vms i.e. memory usage
    listOfProcObjects = sorted(
        listOfProcObjects, key=lambda procObj: procObj["vms"], reverse=True
    )
    return listOfProcObjects


def get_all_memory_usage():
    # Getting all memory using os.popen()
    total_memory, used_memory, free_memory = map(
        int, os.popen("free -t -m").readlines()[-1].split()[1:]
    )

    # Memory usage
    logger.info(f"RAM memory % used: {round((used_memory / total_memory) * 100, 2)}")
    return round((used_memory / total_memory) * 100, 2)


if __name__ == "__main__":
    if platform.system().lower() == "linux":
        Watcher()

    while True:
        time.sleep(10)
        listOfRunningProcess = getListOfProcessSortedByMemory()
        if get_all_memory_usage() > 50:
            for elem in listOfRunningProcess[:20]:
                # print(elem)
                logger.info(elem)
import datetime as dt
from datetime import timedelta

print(dt.datetime(year=2022, month=12, day=2, hour=18, minute=0, second=0, microsecond=0) - timedelta(hours=8))

視化神器Highcharts

from highcharts import Highchart
import datetime
from IPython.display import HTML, display
import yfinance as yf
import os

# 取得股價歷史資料(含臺股\美股\加密貨幣)

symbol = "2330.TW"  # 臺股上市:TW 臺股上櫃:TWO
start = "2018-01-01"  # 起始時間
end = "2022-12-31"  # 結束時間

ohlcv = yf.Ticker(symbol).history("max").loc[start:end]


# 客製化調整參數
color = "#4285f4"  # 線的顏色 (red/green/blue/purple)
linewidth = 2  # 線的粗細
title = symbol  # 標題名稱
width = 800  # 圖的寬度
height = 500  # 圖的高度

# 繪圖設定
H = Highchart(width=width, height=height)

x = ohlcv.index
y = round(ohlcv.Close, 2)

data = [[index, s] for index, s in zip(x, y)]
H.add_data_set(data, "line", "data", color=color)

H.set_options("xAxis", {"type": "datetime"})
H.set_options("title", {"text": title, "style": {"color": "black"}})  # 設定title
H.set_options(
    "plotOptions", {"line": {"lineWidth": linewidth, "dataLabels": {"enabled": False}}}
)  # 設定線的粗度
H.set_options("tooltip", {"shared": True, "crosshairs": True})  # 設定為可互動式

# 顯示圖表
H.save_file("chart")
display(HTML("chart.html"))
os.remove("chart.html")

Test Redis

import redis

botID = 101
r = redis.StrictRedis(host="localhost", port=6379, db=2)

r.hset(
    f"BOT_INFO:{botID}", mapping={"grid_step": "50"},
)

fee = 0.1
r.hset(
    f"BOT_INFO:{botID}", mapping={"spotman_grid_fee": str(fee)},
)

print(r.exists("BOT_INFO:28"))
if not r.hexists(f"BOT_INFO:{botID}", "spotman_grid_fee") or (
    r.hexists(f"BOT_INFO:{botID}", "spotman_grid_fee")
    and float(r.hget(f"BOT_INFO:{botID}", "spotman_grid_fee")) == 0.0
):
    spotman_grid_fee = 0.0
else:
    spotman_grid_fee = fee

print(spotman_grid_fee)

# r.set("foo", "bar")
## print(r.get('foo'))
#
# botID = 28
# r.hset(
#    f"BOT_INFO:{botID}", mapping={"spotman_grid_fee": 0},
# )
#
# spotman_grid_fee = float(r.hget(f"BOT_INFO:{botID}", "spotman_grid_fee"))yy
# print(spotman_grid_fee == 0.0)
#
# print(r.hexists(f"BOT_INFO:{botID}", "spotman_grid_fee"))
# print(r.hget(f"BOT_INFO:{botID}", "grid_step"))


# if r.hexists(
#    f"BOT_INFO:{botID}", "bot_id"
# ):

# botID = 6804
# r = redis.StrictRedis(
#    host="staging-redis-trading.ruv0v5.ng.0001.apne1.cache.amazonaws.com",
#    port=6379,
#    db=1,
# )
# print(r.hget(f"BOT_INFO:{botID}", "grid_step"))
# print(not r.hexists(f"BOT_INFO:{botID}", "spotman_grid_fee"))

[Python爬蟲教學]有效利用Python網頁爬蟲爬取免費的Proxy IP清單

import requests
import re


def get_proxy():
    # https://www.learncodewithmike.com/2021/10/python-scrape-free-proxy-ip.html?m=1
    response = requests.get("https://www.sslproxies.org/")
    proxy_ips = re.findall("\d+\.\d+\.\d+\.\d+:\d+", response.text)  # 「\d+」代表數字一個位數以上
    valid_ips = []

    for ip in proxy_ips:
        try:
            result = requests.get(
                "https://ip.seeip.org/jsonip?",
                proxies={"http": ip, "https": ip},
                timeout=5,
            )
            print(result.json())
            valid_ips.append(ip)
        except:
            print(f"{ip} invalid")

    with open("proxy_list.txt", "w") as file:
        for ip in valid_ips:
            file.write(ip + "\n")
        file.close()


if __name__ == "__main__":
    # get_proxy()
    proxy_dict = {}
    with open("./proxy_list.txt") as f:
        proxy_list = f.read().splitlines()
        # print(proxy_list, type(proxy_list))
    """代理IP地址(高匿)"""
    #proxys = {
    #    "http": "http://118.27.113.167:8080",
    #    "https": "https://118.27.113.167:8080",
    #}
    """head 資訊"""
    head = {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
        "Connection": "keep-alive",
    }
    """http://icanhazip.com會返回當前的IP地址"""
    for proxy in proxy_list:
        proxy_dict["http"] = "http://" + proxy
        proxy_dict["https"] = "https://" + proxy
        try:
            p = requests.get("http://icanhazip.com", headers=head, proxies=proxy_dict, timeout=5)
            print(p.text)
        except Exception as e:
            print(e)
            continue

同學,你的多線程可別再亂 join 了!

如果你在網上搜索「Python 多線程」,那麼你會看到很多文章裡面用到了一個關鍵詞,叫做.join()。但是很多人的代碼裡面都在亂用 join(),例如:

import time
import threading

def sleep_5_seconds():
    time.sleep(5)
    print('睡眠5秒結束')

def sleep_3_seconds():
    time.sleep(3)
    print('睡眠3秒結束')



def sleep_8_seconds():
    time.sleep(8)
    print('睡眠8秒結束')


thread_1 = threading.Thread(target=sleep_8_seconds)
thread_2 = threading.Thread(target=sleep_5_seconds)
thread_3 = threading.Thread(target=sleep_5_seconds)
thread_1.start()
thread_2.start()
thread_3.start()
thread_1.join()
thread_2.join()
thread_3.join()


運行效果如下圖所示:

更有甚者,這樣寫代碼:

thread_1.start() thread_1.join() thread_2.start() thread_2.join() thread_3.start() thread_3.join()

運行效果如下圖所示:

發現三個線程是串行執行的,要運行一共8+5+3=16秒才能結束,於是得出結論——Python 由於有 GIL 鎖的原因,所以多線程是一個線程運行完才運行另一個線程。

抱有這種想法的人,是根本不知道.join()有什麼用,就在跟著別人亂用,以為只要使用多線程,那麼每個線程都必須要 join。

實際上,根本不是這樣的,你只需要 join運行時間最長的那個線程就可以了:

你會發現這樣的運行效果,跟每個線程 join 一次是完全一樣的。

要理解這個問題,我們需要知道,join 有什麼作用。

當我們沒有 join 的時候,我們會發現子線程似乎也能正常運行,如下圖所示:

三個子線程啟動以後,主線程會繼續運行後面的代碼。

那 join 到底有什麼用呢?join 會卡住主線程,並讓當前已經 start 的子線程繼續運行,直到調用.join的這個線程運行完畢。

所以,如果代碼寫為:

thread_1.start() thread_1.join() thread_2.start() thread_2.join() thread_3.start() thread_3.join()

當代碼運行到thread_1.join()時,主線程就卡住了,後面的thread_2.start()根本沒有執行。此時當前只有 thread_1執行過.start()方法,所以此時只有 thread_1再運行。這個線程需要執行8秒鐘。等8秒過後,thread_1結束,於是主線程才會運行到thread_2.start(),第二個線程才會開始運行。所以這個例子裡面,三個線程串行運行,完全是寫代碼的人有問題,而不是什麼 GIL 鎖的問題。

而當我們把代碼寫為:

thread_1.start() thread_2.start() thread_3.start() thread_1.join() thread_2.join() thread_3.join()

當代碼執行到thread_1.join()時,當前三個子線程均已經執行過.start()方法了,所以此時主線程雖然卡住了,但是三個子線程會繼續運行。其中線程3先結束,然後線程2結束。此時線程1還剩3秒鐘,所以此時thread_1.join()依然是卡住的狀態,直到線程1結束,thread_1.join()解除阻塞,代碼運行到thread_2.join()中,但由於thread_2早就結束了,所以這行代碼一閃而過,不會卡住。同理,thread_3.join()也是一閃而過。所以整個過程中,thread_2.join()和thread_3.join()根本沒有起到任何作用。直接就結束了。

所以,你只需要 join 時間最長的這個線程就可以了。時間短的線程沒有 join 的必要。根本不需要把這麼多個 join 堆在一起。

為什麼會有 join 這個功能呢?我們設想這樣一個場景。你的爬蟲使用10個線程爬取100個 URL,主線程需要等到所有URL 都已經爬取完成以後,再來分析數據。此時就可以通過 join 先把主線程卡住,等到10個子線程全部運行結束了,再用主線程進行後面的操作。

那麼可能有人會問,如果我不知道哪個線程先運行完,那個線程後運行完怎麼辦?這個時候是不是就要每個線程都執行 join 操作了呢?

確實,這種情況下,每個線程使用 join是合理的:

thread_list = []
for _ in range(10):
    thread = threading.Thread(target=xxx, args=(xxx, xxx)) 換行thread.start()
    thread_list.append(thread)

for thread in thread_list:
    thread.join()

監控指定Process狀態

import threading, logging, time
import multiprocessing
import psutil


class Producer(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.stop_event = threading.Event()

    def stop(self):
        self.stop_event.set()

    def run(self):
        while not self.stop_event.is_set():
            # print("Producer is working...")
            time.sleep(1)


class Consumer(multiprocessing.Process):
    def __init__(self):
        multiprocessing.Process.__init__(self)
        self.stop_event = multiprocessing.Event()

    def stop(self):
        self.stop_event.set()

    def run(self):
        while not self.stop_event.is_set():
            print("Consumer is working...")
            time.sleep(10)
            try:
                a = 1 / 0
            except Exception as ex:
                print(ex)
                continue


class Monitor(multiprocessing.Process):
    def __init__(self, target_pid):
        multiprocessing.Process.__init__(self)
        self.stop_event = multiprocessing.Event()
        self.target_pid = target_pid

    def stop(self):
        self.stop_event.set()

    def run(self):
        while not self.stop_event.is_set():
            p = psutil.Process(self.target_pid)
            print("Monitor is working...", self.target_pid, p.status)
            time.sleep(1)


def main():
    tasks = [Producer(), Consumer()]

    for t in tasks:
        t.start()

    print(tasks[1].pid)
    t = Monitor(tasks[1].pid)
    t.start()
    time.sleep(3600)

    for task in tasks:
        task.stop()

    for task in tasks:
        task.join()


if __name__ == "__main__":
    logging.basicConfig(
        format="%(asctime)s.%(msecs)s:%(name)s:%(thread)d:%(levelname)s:%(process)d:%(message)s",
        level=logging.INFO,
    )
    main()

檔案拆成1.5G 一個資料夾

#!/bin/env python3

from pathlib import Path
import os
import shutil

number = 0
current_size = 0
pattern = "new-folder-%03d"

new_directory = pattern % number
Path(pattern % number).mkdir(parents=True, exist_ok=True)

# 預設抓 /path/picture/* 該目錄內檔案,若是要包含子目錄,請使用像是 rglob('*.jpg') 替代
for item in Path("Camera").rglob("*"):
    current_size += item.stat().st_size
    if current_size >= 1024 * 1024 * 1500:
        number += 1
        current_size = 0
        new_directory = pattern % number
        Path(new_directory).mkdir(parents=True, exist_ok=True)
    shutil.move(
        os.path.join(item.parent, item.name), os.path.join(new_directory, item.name)
    )

刪除指定日期之前的Row

import pandas as pd

csiti = 23454
units = list(range(0, 400))
begin_date = '2019-10-16'

df = pd.DataFrame({'csiti':csiti, 
                   'units':units,
                   'forecast_date':pd.date_range(begin_date, periods=len(units), freq='1S')})


df.set_index("forecast_date", inplace=True)


df.index = pd.to_datetime(df.index)
print(df)

res = df[~(df.index < '2019-10-16 00:06')]
print(id(res))
# print(res, type(res.index.tolist()[-1]))


# Insert row to dataframe
res.loc[res.index.tolist()[-1]] = [12345, 362]
print(res)
print(id(res))

# Check if a date index exist in Pandas dataframe
print(pd.to_datetime('2019-10-16 00:06') not in df.index)
print(pd.to_datetime('2019-10-16 00:07') not in df.index)

yfinance

import yfinance as yf

dji_data = yf.download(tickers = "^DJI", interval = "1d", period = "10d")
dji_data['Rets'] = round(dji_data['Close'].pct_change() * 100, 2)
print(dji_data)

sox_data = yf.download(tickers="^SOX", interval = "1d", period="10d")
sox_data['Rets'] = round(sox_data['Close'].pct_change() * 100, 2)
print(sox_data)

ixic_data = yf.download(tickers="^IXIC", interval = "1d", period="10d")
ixic_data['Rets'] = round(ixic_data['Close'].pct_change() * 100, 2)
print(ixic_data)

plotly 畫K bars

# Raw Package
import numpy as np
import pandas as pd
from pandas_datareader import data as pdr

# Market Data 
import yfinance as yf

#Graphing/Visualization
import datetime as dt 
import plotly.graph_objs as go 

# Override Yahoo Finance 
yf.pdr_override()

# Create input field for our desired stock 
stock=input("Enter a stock ticker symbol: ")

# Retrieve stock data frame (df) from yfinance API at an interval of 1m 
df = yf.download(tickers=stock,period='1d',interval='1m')

print(df)

# Declare plotly figure (go)
fig=go.Figure()

fig.add_trace(go.Candlestick(x=df.index,
                open=df['Open'],
                high=df['High'],
                low=df['Low'],
                close=df['Close'], name = 'market data'))

fig.update_layout(
    title= str(stock)+' Live Share Price:',
    yaxis_title='Stock Price (USD per Shares)')               

fig.update_xaxes(
    rangeslider_visible=True,
    rangeselector=dict(
        buttons=list([
            dict(count=15, label="15m", step="minute", stepmode="backward"),
            dict(count=45, label="45m", step="minute", stepmode="backward"),
            dict(count=1, label="HTD", step="hour", stepmode="todate"),
            dict(count=3, label="3h", step="hour", stepmode="backward"),
            dict(step="all")
        ])
    )
)

fig.show()
import asyncio
import aiohttp
import multiprocessing
import threading


async def get_thread_id():
    return (multiprocessing.current_process().pid, threading.get_ident())


def get_exchange_rate(url, exchange_name, conn):
    if exchange_name == "RYBIT":
        asyncio.run(handle_rybit_exchange(url, conn))
    elif exchange_name == "ACE":
        asyncio.run(handle_ace_exchange(url, conn))
    elif exchange_name == "MAX":
        asyncio.run(handle_max_exchange(url, conn))
    elif exchange_name == "BITOPRO":
        asyncio.run(handle_bitopro_exchange(url, conn))
    else:
        print(f"Unknown exchange: {exchange_name}")


async def handle_rybit_exchange(url, conn):
    while True:
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                if response.status == 200:
                    data = await response.json()
                    buy_rate = data.get("data").get("buy_rate")
                    sell_rate = data.get("data").get("sell_rate")
                    process_id, thread_id = await get_thread_id()
                    print(f"RYBIT Process ID:{process_id}, Thread ID:{thread_id}")
                    print("RYBIT 買入匯率:", buy_rate)
                    print("RYBIT 賣出匯率:", sell_rate)
                    conn.send(("RYBIT", buy_rate, sell_rate))
                else:
                    print("RYBIT 發生錯誤,HTTP 狀態碼為:", response.status)
        await asyncio.sleep(1)


async def handle_ace_exchange(url, conn):
    while True:
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                if response.status == 200:
                    data = await response.json()
                    process_id, thread_id = await get_thread_id()
                    print(f"ACE Process ID:{process_id}, Thread ID:{thread_id}")
                    print(f"ACE USDT/TWD 買入委託價格: {data['orderbook']['bids'][0][1]}")
                    print(f"ACE USDT/TWD 賣出委託價格: {data['orderbook']['asks'][0][1]}")
                    conn.send(("ACE", data["orderbook"]["bids"][0][1], data["orderbook"]["asks"][0][1]))
                else:
                    print("ACE 發生錯誤,HTTP 狀態碼為:", response.status)
        await asyncio.sleep(1)


async def handle_max_exchange(url, conn):
    while True:
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                if response.status == 200:
                    data = await response.json()
                    usdt_twd = data.get("usdttwd")
                    buy_price = usdt_twd.get("buy")
                    sell_price = usdt_twd.get("sell")
                    process_id, thread_id = await get_thread_id()
                    print(f"MAX Process ID:{process_id}, Thread ID:{thread_id}")
                    print(f"MAX USDT/TWD buy: {buy_price}")
                    print(f"MAX USDT/TWD sell: {sell_price}")
                    conn.send(("MAX", buy_price, sell_price))
                else:
                    print("MAX 發生錯誤,HTTP 狀態碼為:", response.status)
        await asyncio.sleep(1)


async def handle_bitopro_exchange(url, conn):
    while True:
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                if response.status == 200:
                    data = await response.json()
                    bid_price = data["bids"][0]["price"]
                    ask_price = data["asks"][0]["price"]
                    process_id, thread_id = await get_thread_id()
                    print(f"BITOPRO Process ID:{process_id}, Thread ID:{thread_id}")
                    print(f"BITOPRO 買入委託價格:{bid_price}")
                    print(f"BITOPRO 賣出委託價格:{ask_price}")
                    conn.send(("BITOPRO", bid_price, ask_price))
                else:
                    print("BITOPRO 發生錯誤,HTTP 狀態碼為:", response.status)
        await asyncio.sleep(1)


def collect_exchange_rates(conn):
    exchange_rates = {}
    while True:
        exchange_name, buy_rate, sell_rate = conn.recv()
        exchange_rates[exchange_name] = {"buy": buy_rate, "sell": sell_rate}
        print(f"Exchange rates: {exchange_rates}")

async def main():
    exchanges = [
        {
            "name": "RYBIT",
            "url": "https://www.rybit.com/wallet-api/v1/kgi/exchange-rates/?symbol=USDT_TWD",
        },
        {
            "name": "ACE",
            "url": "https://ace.io/polarisex/oapi/list/orderBooks/USDT/TWD",
        },
        {"name": "MAX", "url": "https://max-api.maicoin.com/api/v2/tickers"},
        {"name": "BITOPRO", "url": "https://api.bitopro.com/v3/order-book/usdt_twd"},
    ]
    parent_conn, child_conn = multiprocessing.Pipe()
    processes = []
    for exchange in exchanges:
        p = multiprocessing.Process(
            target=get_exchange_rate, args=(exchange["url"], exchange["name"], child_conn)
        )
        processes.append(p)
        p.start()

    collector = multiprocessing.Process(target=collect_exchange_rates, args=(parent_conn,))
    collector.start()

    for p in processes:
        p.join()


if __name__ == "__main__":
    asyncio.run(main())

Thread 共用變數

import threading

# 自定義的類別,包含多個共享變數或字段
class SharedData:
    def __init__(self):
        self.variable1 = 0
        self.variable2 = "Hello"
        self.variable3 = []

# 定義一個函數,接受共享數據對象作為參數
def modify_shared_data(shared_data, lock):
    for _ in range(1000000):
        # 獲取鎖
        with lock:
            shared_data.variable1 += 1
            shared_data.variable2 = shared_data.variable2.upper()
            shared_data.variable3.append(shared_data.variable1)
        # 釋放鎖

# 創建一個共享數據對象
shared_data = SharedData()

# 創建一個Lock對象,用於線程同步
lock = threading.Lock()

# 創建兩個線程,將共享數據對象和鎖傳遞給它們的函數
thread1 = threading.Thread(target=modify_shared_data, args=(shared_data, lock))
thread2 = threading.Thread(target=modify_shared_data, args=(shared_data, lock))

# 啟動這兩個線程
thread1.start()
thread2.start()

# 等待這兩個線程完成
thread1.join()
thread2.join()

# 檢查共享數據對象的值
print("共享變數1的值:", shared_data.variable1)
print("共享變數2的值:", shared_data.variable2)
print("共享變數3的值:", shared_data.variable3)

Redis 時間鎖

import threading
import redis
import os
import time


class RedisLock:
    def __init__(self, redis_conn, lock_key, timeout=10):
        self.redis_conn = redis_conn
        self.lock_key = lock_key
        self.timeout = timeout

    def acquire(self):
        start_time = time.time()
        tid = threading.get_ident()  # Fetch the Thread ID (TID)

        while True:
            lock_acquired = self.redis_conn.setnx(self.lock_key, "locked")

            if lock_acquired:
                self.redis_conn.expire(self.lock_key, self.timeout)
                self.pid = os.getpid()  # Fetch the Process ID (PID) during initialization
                self.tid = tid  # Store Thread ID (TID) for printing in release
                print(f"PID:{self.pid} TID:{tid} acquired the lock.")
                return True
            else:
                time.sleep(0.1)

            elapsed_time = time.time() - start_time
            if elapsed_time > self.timeout:
                return False

    def release(self):
        tid = self.tid  # Fetch the stored Thread ID (TID) during release
        print(f"PID:{self.pid} TID:{tid} released the lock.")
        self.redis_conn.delete(self.lock_key)

    def __enter__(self):
        if not self.acquire():
            raise RuntimeError("Could not acquire the lock.")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.release()


def worker(lock, thread_id):
    print(f"Thread-{thread_id} is trying to acquire the lock.")
    with lock:
        print(
            f"Thread-{thread_id} acquired the lock. Performing some critical section operations."
        )
        time.sleep(2)  # Simulating some critical section operations
    print(f"Thread-{thread_id} released the lock.")


# Redis connection setup
redis_conn = redis.StrictRedis(host="localhost", port=6379, db=0)
lock_key = "my_lock"
redis_lock = RedisLock(redis_conn, lock_key)

# Creating threads and starting them
thread1 = threading.Thread(target=worker, args=(redis_lock, 1))
thread2 = threading.Thread(target=worker, args=(redis_lock, 2))

thread1.start()
thread2.start()

thread1.join()
thread2.join()
import multiprocessing
import redis
import os
import time


class RedisLock:
    def __init__(self, redis_conn, lock_key, timeout=10):
        self.redis_conn = redis_conn
        self.lock_key = lock_key
        self.timeout = timeout

    def acquire(self):
        start_time = time.time()
        while True:
            lock_acquired = self.redis_conn.set(
                self.lock_key, "locked", ex=self.timeout, nx=True
            )
            if lock_acquired:
                self.pid = os.getpid()  # 取得初始化時的 Process ID (PID)
                print(f"PID:{self.pid} acquired the lock.")
                return True
            else:
                time.sleep(0.1)
            elapsed_time = time.time() - start_time
            if elapsed_time > self.timeout:
                return False

    def release(self):
        print(f"PID:{self.pid} released the lock.")
        self.redis_conn.delete(self.lock_key)

    def __enter__(self):
        if not self.acquire():
            raise RuntimeError("Could not acquire the lock.")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.release()


def worker(lock, process_id):
    print(f"Process-{process_id} is trying to acquire the lock.")
    with lock:
        print(
            f"Process-{process_id} acquired the lock. Performing some critical section operations."
        )
        time.sleep(2)  # 模擬一些關鍵區段操作
    print(f"Process-{process_id} released the lock.")


# Redis 連線設定
redis_conn = redis.StrictRedis(host="localhost", port=6379, db=0)
lock_key = "my_lock"
redis_lock = RedisLock(redis_conn, lock_key)

# 創建並啟動進程
process1 = multiprocessing.Process(target=worker, args=(redis_lock, 1))
process2 = multiprocessing.Process(target=worker, args=(redis_lock, 2))

process1.start()
process2.start()

process1.join()
process2.join()

透過Queue 監控 thread 重啟

import threading
import time
import inspect
import ctypes

import threading
import time
import queue
import random


class WorkerThread(threading.Thread):
    def __init__(self, msg_queue, thread_no):
        threading.Thread.__init__(self)
        self.msg_queue = msg_queue
        self.thread_no = thread_no

    def run(self):
        print("Worker thread started, thread no: ", self.thread_no)
        while True:
            self.msg_queue.put("Running")
            sleep_time = random.randint(5, 12)
            print(
                f"Worker thread is running... {threading.get_ident()}, sleeping for {sleep_time} seconds"
            )
            time.sleep(sleep_time)


def restart_thread(worker_thread, status_queue, thread_no):
    print("Worker thread exited. Restarting...")
    worker_thread = WorkerThread(status_queue, thread_no)
    worker_thread.start()
    return worker_thread


def _async_raise(tid, exctype):
    """Raises an exception in the threads with id tid"""
    if not inspect.isclass(exctype):
        raise TypeError("Only types can be raised (not instances)")
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
        ctypes.c_long(tid), ctypes.py_object(exctype)
    )
    if res == 0:
        raise ValueError("invalid thread id")
    elif res != 1:
        # """if it returns a number greater than one, you're in trouble,
        # and you should call it again with exc=NULL to revert the effect"""
        ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
        raise SystemError("PyThreadState_SetAsyncExc failed")


def stop_thread(thread):
    _async_raise(thread.ident, SystemExit)


def main():
    thread_no = 0
    status_queue = queue.Queue()
    worker_thread = WorkerThread(status_queue, thread_no)
    worker_thread.start()

    # 主線程監控
    while True:
        try:
            status = status_queue.get(timeout=10)
            print(status)
        except queue.Empty:
            stop_thread(worker_thread)
            thread_no += 1
            worker_thread = restart_thread(worker_thread, status_queue, thread_no)


if __name__ == "__main__":
    main()

讀取檔案中網址

import re

def extract_urls_from_file(file_path):
    urls = []
    with open(file_path, 'r', encoding='utf-8') as file:
        for line in file:
            urls_in_line = re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', line)
            urls.extend(urls_in_line)
    return urls

file_path = './bb'  

urls = extract_urls_from_file(file_path)

for url in urls:
    print(url)

事件驅動event實現

from queue import Queue, Empty
from threading import Thread


class EventManager:
    def __init__(self):
        self._event_queue = Queue()
        self._active = False
        self._thread = Thread(target=self._run)
        self._count = 0
        self._handlers = {}

    def _run(self):
        while self._active:
            try:
                event = self._event_queue.get(block=True, timeout=1)
                self._event_process(event)
            except Empty:
                pass
            self._count += 1

    def _event_process(self, event):
        if event.type_ in self._handlers:
            for handler in self._handlers[event.type_]:
                handler(event)
        self._count += 1

    def start(self):
        self._active = True
        self._thread.start()
        self._count += 1

    def stop(self):
        self._active = False
        self._thread.join()
        self._count += 1

    def add_event_listener(self, type_, handler):
        handler_list = self._handlers.get(type_, [])
        if handler not in handler_list:
            handler_list.append(handler)
            self._handlers[type_] = handler_list
        self._count += 1

    def remove_event_listener(self, type_, handler):
        try:
            handler_list = self._handlers[type_]
            if handler in handler_list:
                handler_list.remove(handler)
                if not handler_list:
                    del self._handlers[type_]
        except KeyError:
            pass
        self._count += 1

    def send_event(self, event):
        self._event_queue.put(event)
        self._count += 1


class Event:
    def __init__(self, type_=None, args_=None):
        self.type_ = type_
        self.args = args_


###############################################################################

# 定義事件類型
EVENT_TURN_START = "Turn_start"
EVENT_BROADCAST = "Broadcast"
EVENT_UPDATE = "Update"
EVENT_DRAW_CARD = "Draw_card"
EVENT_TURN_END = "Turn_end"
EVENT_HEARTBEAT = "Heartbeat"


# 事件處理函數 (玩家)
class Player:
    def __init__(self, id):
        self._id = id

    def turn_start(self, event):
        print(f"{self._id} 回合開始")

    def broadcast(self, event):
        print(f"{self._id} 廣播訊息")

    def update(self, event):
        print(f"{self._id} 更新(場面資料)")

    def draw_card(self, event):
        print(f"{self._id} 玩家抽牌")

    def turn_end(self, event):
        print(f"{self._id} 回合結束")

    def heartbeat(self, event):
        print(f"{self._id} 心跳訊號")


def test():
    player1 = Player("player one")
    event_manager = EventManager()
    event_manager.add_event_listener(EVENT_TURN_START, player1.turn_start)
    event_manager.add_event_listener(EVENT_BROADCAST, player1.broadcast)
    event_manager.add_event_listener(EVENT_UPDATE, player1.update)
    event_manager.add_event_listener(EVENT_DRAW_CARD, player1.draw_card)
    event_manager.add_event_listener(EVENT_TURN_END, player1.turn_end)
    event_manager.add_event_listener(EVENT_HEARTBEAT, player1.heartbeat)
    event_manager.start()

    send = make_sender(event_manager)  # 創建 sender 傳送事件
    send(Event(type_=EVENT_TURN_START))
    send(Event(type_=EVENT_BROADCAST))
    send(Event(type_=EVENT_UPDATE))
    send(Event(type_=EVENT_DRAW_CARD))
    send(Event(type_=EVENT_TURN_END))
    send(Event(type_=EVENT_HEARTBEAT))


def make_sender(event_manager):
    em = event_manager

    def send(event):
        return em.send_event(event)

    return send


if __name__ == "__main__":
    test()
import traceback
import faulthandler
import ctypes



def test_segmentation_fault():
    # 對於segmentation fault並不能catch到異常,即此處try沒效果
    try:
        ctypes.string_at(0)
    except Exception as e:
        print(traceback.format_exc())


if __name__ == "__main__":
    faulthandler.enable()
    test_segmentation_fault()
"""
這個設計的主要特點:

使用asyncio實現異步編程,提高效能和並發性。
DataPublisher類實現了發布-訂閱模式,允許多個策略訂閱tick和orderbook數據。
使用asyncio.Queue作為數據緩衝,確保數據接收不會被策略處理阻塞。
每個策略都是獨立的對象,可以獨立處理接收到的數據。
主循環中的asyncio.create_task()確保數據處理在後臺運行,不會阻塞主程序。

這個設計允許高效地接收和處理tick和orderbook數據,同時支持多個策略並發運行。你可以根據實際需求進一步優化和擴展這個框架,例如添加錯誤處理、日誌記錄、性能監控等功能。
"""

import asyncio
from collections import deque
from typing import Dict, List, Callable


class DataPublisher:
    def __init__(self):
        self.tick_subscribers: List[Callable] = []
        self.orderbook_subscribers: List[Callable] = []
        self.tick_queue = asyncio.Queue()
        self.orderbook_queue = asyncio.Queue()

    def subscribe_tick(self, callback: Callable):
        self.tick_subscribers.append(callback)

    def subscribe_orderbook(self, callback: Callable):
        self.orderbook_subscribers.append(callback)

    async def publish_tick(self, tick_data):
        await self.tick_queue.put(tick_data)

    async def publish_orderbook(self, orderbook_data):
        await self.orderbook_queue.put(orderbook_data)

    async def process_tick_queue(self):
        while True:
            tick_data = await self.tick_queue.get()
            for subscriber in self.tick_subscribers:
                await subscriber(tick_data)

    async def process_orderbook_queue(self):
        while True:
            orderbook_data = await self.orderbook_queue.get()
            for subscriber in self.orderbook_subscribers:
                await subscriber(orderbook_data)


class Strategy:
    def __init__(self, name: str):
        self.name = name

    async def on_tick(self, tick_data):
        print(f"Strategy {self.name} received tick: {tick_data}")

    async def on_orderbook(self, orderbook_data):
        print(f"Strategy {self.name} received orderbook: {orderbook_data}")


async def main():
    publisher = DataPublisher()

    # 創建多個策略
    strategies = [Strategy(f"Strategy{i}") for i in range(3)]

    # 訂閱數據
    for strategy in strategies:
        publisher.subscribe_tick(strategy.on_tick)
        publisher.subscribe_orderbook(strategy.on_orderbook)

    # 啟動數據處理任務
    asyncio.create_task(publisher.process_tick_queue())
    asyncio.create_task(publisher.process_orderbook_queue())

    # 模擬接收數據
    for i in range(10):
        await publisher.publish_tick(f"Tick {i}")
        await publisher.publish_orderbook(f"Orderbook {i}")
        await asyncio.sleep(0.1)


if __name__ == "__main__":
    asyncio.run(main())

線程模型:

優勢: 使用線程可以利用多核 CPU 的優勢,在某些情況下提高併發任務的處理速度。 每個線程獨立運行,不會相互影響,因此適合處理獨立的 IO 任務,如 WebSocket 數據處理。 劣勢: 線程開銷較大,尤其是在大量線程的情況下,可能導致上下文切換開銷增大,影響整體性能。 需要處理線程同步和線程安全問題。

import asyncio
import websockets
import os
import json
import threading
import queue
import pandas as pd
from collections import deque
from concurrent.futures import ThreadPoolExecutor
from loguru import logger


def print_pid_tid(tag):
    print(f"{tag} PID: {os.getpid()}, TID: {threading.get_ident()}")


class DataPublisher:
    def __init__(self, tick_queue, orderbook_queue):
        symbol = "btcusdt"
        self.tick_url = f"wss://stream.binance.com:9443/ws/{symbol}@trade"
        self.orderbook_url = f"wss://stream.binance.com:9443/ws/{symbol}@depth20@100ms"
        self.tick_queue = tick_queue
        self.orderbook_queue = orderbook_queue

    def _start_websocket(self, url, queue):
        asyncio.run(self._websocket_handler(url, queue))

    async def _websocket_handler(self, url, queue):
        async with websockets.connect(url) as websocket:
            while True:
                print_pid_tid(url)
                response = await websocket.recv()
                data = json.loads(response)
                # print(f"Received Data: {data}")
                queue.put(data)  # Put data into the queue (no await)

    def start(self):
        # Start tick and order book WebSocket connections in separate threads
        threading.Thread(
            target=self._start_websocket,
            args=(self.tick_url, self.tick_queue),
            daemon=True,
        ).start()
        threading.Thread(
            target=self._start_websocket,
            args=(self.orderbook_url, self.orderbook_queue),
            daemon=True,
        ).start()


class DataProcessor:
    def __init__(self, event_loop, tick_queue, orderbook_queue):
        self.kline_subscribers = []
        self.current_kline = {
            "open": None,
            "high": None,
            "low": None,
            "close": None,
            "volume": 0,
            "start_time": None,
        }
        self.kline_df = pd.DataFrame(
            columns=["timestamp", "open", "high", "low", "close", "volume"]
        )
        self.orderbook_history = deque(maxlen=1000)
        self.latest_tick = None
        self.latest_orderbook = None
        self.max_kline_history = 1000  # Limit K-line history to 1000 records
        self.kline_lock = threading.Lock()
        self.tick_data_list = deque(maxlen=5000)  # Collect tick data
        self.executor = ThreadPoolExecutor(max_workers=4)  # Create thread pool
        self.event_loop = event_loop
        self.tick_queue = tick_queue
        self.orderbook_queue = orderbook_queue

    def subscribe_kline(self, callback):
        self.kline_subscribers.append(callback)

    def process_ticks(self):
        while True:
            tick_data = self.tick_queue.get()  # Get tick data from the queue
            print_pid_tid("process_ticks")
            # self.on_tick(tick_data)

    def process_orderbooks(self):
        while True:
            orderbook_data = (
                self.orderbook_queue.get()
            )  # Get orderbook data from the queue
            print_pid_tid("process_orderbooks")
            # self.on_orderbook(orderbook_data)

    def on_tick(self, tick_data):
        print_pid_tid("DataProcessor on_tick")
        self.latest_tick = tick_data
        self.tick_data_list.append(
            {"datetime": tick_data["T"], "close": tick_data["p"]}
        )

        # Use thread pool to process K-line calculation
        future = self.executor.submit(self.update_and_publish_kline)
        future.add_done_callback(self._handle_thread_result)

    def on_orderbook(self, orderbook_data):
        print_pid_tid("DataProcessor on_orderbook")
        self.latest_orderbook = orderbook_data
        self.orderbook_history.append(orderbook_data)

    def update_and_publish_kline(self):
        print_pid_tid("DataProcessor update_and_publish_kline")
        try:
            # Convert tick data to DataFrame
            tick_df = pd.DataFrame(list(self.tick_data_list))
            tick_df.set_index("datetime", inplace=True)
            tick_df.index = pd.to_datetime(tick_df.index, unit="ms")

            now = pd.Timestamp.now(tz="UTC")
            current_minute_start = now.floor("T").to_datetime64()

            # Only process the latest 1 minute of data
            recent_ticks = tick_df.loc[tick_df.index >= current_minute_start]

            # If there is sufficient data, calculate 1-minute K-line data
            if not recent_ticks.empty:
                futures_1min_kbars = recent_ticks["close"].resample("1T").ohlc()

                with self.kline_lock:
                    for timestamp, kline in futures_1min_kbars.iterrows():
                        self.current_kline = {
                            "open": kline["open"],
                            "high": kline["high"],
                            "low": kline["low"],
                            "close": kline["close"],
                            "volume": 0,  # Assume no volume data
                            "start_time": timestamp,
                        }
                        new_kline = pd.DataFrame([self.current_kline])
                        self.kline_df = pd.concat(
                            [self.kline_df, new_kline]
                        ).reset_index(drop=True)

                        # Limit K-line history to the latest 1000 records
                        if len(self.kline_df) > self.max_kline_history:
                            self.kline_df = self.kline_df.iloc[
                                -self.max_kline_history :
                            ]

                        for subscriber in self.kline_subscribers:
                            asyncio.run_coroutine_threadsafe(
                                subscriber(self.current_kline), self.event_loop
                            )

            self.tick_data_list = deque(
                [
                    tick
                    for tick in self.tick_data_list
                    if pd.to_datetime(tick["datetime"], unit="ms").to_datetime64()
                    >= current_minute_start
                ]
            )

        except Exception as e:
            logger.error(f"Error in update_and_publish_kline: {e}")

    def _handle_thread_result(self, future):
        try:
            future.result()
        except Exception as e:
            logger.error(f"Thread processing error: {e}")

    def get_latest_data(self):
        with self.kline_lock:
            return {
                "latest_tick": self.latest_tick,
                "latest_kline": (
                    self.kline_df.iloc[-1] if not self.kline_df.empty else None
                ),
                "kline_history": self.kline_df,
                "latest_orderbook": self.latest_orderbook,
                "orderbook_history": list(self.orderbook_history),
            }


async def main():
    tick_queue = queue.Queue()
    orderbook_queue = queue.Queue()
    event_loop = asyncio.get_event_loop()

    data_processor = DataProcessor(event_loop, tick_queue, orderbook_queue)
    data_publisher = DataPublisher(tick_queue, orderbook_queue)
    data_publisher.start()  # Start DataPublisher in a separate thread

    loop = asyncio.get_event_loop()

    # Process both ticks and order books in separate threads
    threading.Thread(target=data_processor.process_ticks, daemon=True).start()
    threading.Thread(target=data_processor.process_orderbooks, daemon=True).start()

    while True:
        data = data_processor.get_latest_data()
        await asyncio.sleep(1)


if __name__ == "__main__":
    asyncio.run(main())

asyncio.gather 異步模型

優勢: 異步模型的開銷較低,沒有線程切換的開銷,適合大量 IO 密集型任務。 在單線程內管理所有任務,簡化了同步和資源管理。 異步任務之間可以更高效地共享 CPU 資源,避免了不必要的等待時間。 劣勢: 異步模型依賴事件循環,無法充分利用多核 CPU。對於 CPU 密集型任務,性能不如多線程模型。 在處理非常高的併發情況下,如果單線程成為瓶頸,可能導致性能下降。

import asyncio
import websockets
import os
import json
import threading
import queue
import pandas as pd
from collections import deque
from concurrent.futures import ThreadPoolExecutor
from loguru import logger


def print_pid_tid(tag):
    print(f"{tag} PID: {os.getpid()}, TID: {threading.get_ident()}")


class DataPublisher:
    def __init__(self, tick_queue, orderbook_queue):
        symbol = "btcusdt"
        self.tick_url = f"wss://stream.binance.com:9443/ws/{symbol}@trade"
        self.orderbook_url = f"wss://stream.binance.com:9443/ws/{symbol}@depth20@100ms"
        self.tick_queue = tick_queue
        self.orderbook_queue = orderbook_queue

    def _start_websocket(self, url, queue):
        asyncio.run(self._websocket_handler(url, queue))

    async def _websocket_handler(self, url, queue):
        async with websockets.connect(url) as websocket:
            while True:
                print_pid_tid(url)
                response = await websocket.recv()
                data = json.loads(response)
                # print(f"Received Data: {data}")
                queue.put(data)  # Put data into the queue (no await)

    def start(self):
        # Start tick and order book WebSocket connections in separate threads
        threading.Thread(
            target=self._start_websocket,
            args=(self.tick_url, self.tick_queue),
            daemon=True,
        ).start()
        threading.Thread(
            target=self._start_websocket,
            args=(self.orderbook_url, self.orderbook_queue),
            daemon=True,
        ).start()


class DataProcessor:
    def __init__(self, event_loop, tick_queue, orderbook_queue):
        self.kline_subscribers = []
        self.current_kline = {
            "open": None,
            "high": None,
            "low": None,
            "close": None,
            "volume": 0,
            "start_time": None,
        }
        self.kline_df = pd.DataFrame(
            columns=["timestamp", "open", "high", "low", "close", "volume"]
        )
        self.orderbook_history = deque(maxlen=1000)
        self.latest_tick = None
        self.latest_orderbook = None
        self.max_kline_history = 1000  # Limit K-line history to 1000 records
        self.kline_lock = threading.Lock()
        self.tick_data_list = deque(maxlen=5000)  # Collect tick data
        self.executor = ThreadPoolExecutor(max_workers=4)  # Create thread pool
        self.event_loop = event_loop
        self.tick_queue = tick_queue
        self.orderbook_queue = orderbook_queue

    def subscribe_kline(self, callback):
        self.kline_subscribers.append(callback)

    def process_ticks(self):
        while True:
            tick_data = self.tick_queue.get()  # Get tick data from the queue
            print_pid_tid("process_ticks")
            # self.on_tick(tick_data)

    def process_orderbooks(self):
        while True:
            orderbook_data = (
                self.orderbook_queue.get()
            )  # Get orderbook data from the queue
            print_pid_tid("process_orderbooks")
            # self.on_orderbook(orderbook_data)

    def on_tick(self, tick_data):
        print_pid_tid("DataProcessor on_tick")
        self.latest_tick = tick_data
        self.tick_data_list.append(
            {"datetime": tick_data["T"], "close": tick_data["p"]}
        )

        # Use thread pool to process K-line calculation
        future = self.executor.submit(self.update_and_publish_kline)
        future.add_done_callback(self._handle_thread_result)

    def on_orderbook(self, orderbook_data):
        print_pid_tid("DataProcessor on_orderbook")
        self.latest_orderbook = orderbook_data
        self.orderbook_history.append(orderbook_data)

    def update_and_publish_kline(self):
        print_pid_tid("DataProcessor update_and_publish_kline")
        try:
            # Convert tick data to DataFrame
            tick_df = pd.DataFrame(list(self.tick_data_list))
            tick_df.set_index("datetime", inplace=True)
            tick_df.index = pd.to_datetime(tick_df.index, unit="ms")

            now = pd.Timestamp.now(tz="UTC")
            current_minute_start = now.floor("T").to_datetime64()

            # Only process the latest 1 minute of data
            recent_ticks = tick_df.loc[tick_df.index >= current_minute_start]

            # If there is sufficient data, calculate 1-minute K-line data
            if not recent_ticks.empty:
                futures_1min_kbars = recent_ticks["close"].resample("1T").ohlc()

                with self.kline_lock:
                    for timestamp, kline in futures_1min_kbars.iterrows():
                        self.current_kline = {
                            "open": kline["open"],
                            "high": kline["high"],
                            "low": kline["low"],
                            "close": kline["close"],
                            "volume": 0,  # Assume no volume data
                            "start_time": timestamp,
                        }
                        new_kline = pd.DataFrame([self.current_kline])
                        self.kline_df = pd.concat(
                            [self.kline_df, new_kline]
                        ).reset_index(drop=True)

                        # Limit K-line history to the latest 1000 records
                        if len(self.kline_df) > self.max_kline_history:
                            self.kline_df = self.kline_df.iloc[
                                -self.max_kline_history :
                            ]

                        for subscriber in self.kline_subscribers:
                            asyncio.run_coroutine_threadsafe(
                                subscriber(self.current_kline), self.event_loop
                            )

            self.tick_data_list = deque(
                [
                    tick
                    for tick in self.tick_data_list
                    if pd.to_datetime(tick["datetime"], unit="ms").to_datetime64()
                    >= current_minute_start
                ]
            )

        except Exception as e:
            logger.error(f"Error in update_and_publish_kline: {e}")

    def _handle_thread_result(self, future):
        try:
            future.result()
        except Exception as e:
            logger.error(f"Thread processing error: {e}")

    def get_latest_data(self):
        with self.kline_lock:
            return {
                "latest_tick": self.latest_tick,
                "latest_kline": (
                    self.kline_df.iloc[-1] if not self.kline_df.empty else None
                ),
                "kline_history": self.kline_df,
                "latest_orderbook": self.latest_orderbook,
                "orderbook_history": list(self.orderbook_history),
            }


async def main():
    tick_queue = queue.Queue()
    orderbook_queue = queue.Queue()
    event_loop = asyncio.get_event_loop()

    data_processor = DataProcessor(event_loop, tick_queue, orderbook_queue)
    data_publisher = DataPublisher(tick_queue, orderbook_queue)
    data_publisher.start()  # Start DataPublisher in a separate thread

    loop = asyncio.get_event_loop()

    # Process both ticks and order books in separate threads
    threading.Thread(target=data_processor.process_ticks, daemon=True).start()
    threading.Thread(target=data_processor.process_orderbooks, daemon=True).start()

    while True:
        data = data_processor.get_latest_data()
        await asyncio.sleep(1)


if __name__ == "__main__":
    asyncio.run(main())

import asyncio
import websockets
import os
import json
import threading
import queue
import pandas as pd
from collections import deque
from concurrent.futures import ThreadPoolExecutor
from loguru import logger


def print_pid_tid(tag):
    print(f"{tag} PID: {os.getpid()}, TID: {threading.get_ident()}")


class DataPublisher:
    def __init__(self, tick_queue, orderbook_queue):
        symbol = "btcusdt"
        self.tick_url = f"wss://stream.binance.com:9443/ws/{symbol}@trade"
        self.orderbook_url = f"wss://stream.binance.com:9443/ws/{symbol}@depth20@100ms"
        self.tick_queue = tick_queue
        self.orderbook_queue = orderbook_queue

    async def _websocket_handler(self, url, queue):
        async with websockets.connect(url) as websocket:
            while True:
                print_pid_tid(url)
                response = await websocket.recv()
                data = json.loads(response)
                queue.put(data)  # Put data into the queue (no await)

    async def start(self):
        await asyncio.gather(
            self._websocket_handler(self.tick_url, self.tick_queue),
            self._websocket_handler(self.orderbook_url, self.orderbook_queue),
        )


class DataProcessor:
    def __init__(self, event_loop, tick_queue, orderbook_queue):
        self.kline_subscribers = []
        self.current_kline = {
            "open": None,
            "high": None,
            "low": None,
            "close": None,
            "volume": 0,
            "start_time": None,
        }
        self.kline_df = pd.DataFrame(
            columns=["timestamp", "open", "high", "low", "close", "volume"]
        )
        self.orderbook_history = deque(maxlen=1000)
        self.latest_tick = None
        self.latest_orderbook = None
        self.max_kline_history = 1000  # Limit K-line history to 1000 records
        self.kline_lock = threading.Lock()
        self.tick_data_list = deque(maxlen=5000)  # Collect tick data
        self.executor = ThreadPoolExecutor(max_workers=4)  # Create thread pool
        self.event_loop = event_loop
        self.tick_queue = tick_queue
        self.orderbook_queue = orderbook_queue

    def subscribe_kline(self, callback):
        self.kline_subscribers.append(callback)

    def process_ticks(self):
        while True:
            tick_data = self.tick_queue.get()  # Get tick data from the queue
            print_pid_tid("process_ticks")
            # self.on_tick(tick_data)

    def process_orderbooks(self):
        while True:
            orderbook_data = (
                self.orderbook_queue.get()
            )  # Get orderbook data from the queue
            print_pid_tid("process_orderbooks")
            # self.on_orderbook(orderbook_data)

    def on_tick(self, tick_data):
        print_pid_tid("DataProcessor on_tick")
        self.latest_tick = tick_data
        self.tick_data_list.append(
            {"datetime": tick_data["T"], "close": tick_data["p"]}
        )

        # Use thread pool to process K-line calculation
        future = self.executor.submit(self.update_and_publish_kline)
        future.add_done_callback(self._handle_thread_result)

    def on_orderbook(self, orderbook_data):
        print_pid_tid("DataProcessor on_orderbook")
        self.latest_orderbook = orderbook_data
        self.orderbook_history.append(orderbook_data)

    def update_and_publish_kline(self):
        print_pid_tid("DataProcessor update_and_publish_kline")
        try:
            # Convert tick data to DataFrame
            tick_df = pd.DataFrame(list(self.tick_data_list))
            tick_df.set_index("datetime", inplace=True)
            tick_df.index = pd.to_datetime(tick_df.index, unit="ms")

            now = pd.Timestamp.now(tz="UTC")
            current_minute_start = now.floor("T").to_datetime64()

            # Only process the latest 1 minute of data
            recent_ticks = tick_df.loc[tick_df.index >= current_minute_start]

            # If there is sufficient data, calculate 1-minute K-line data
            if not recent_ticks.empty:
                futures_1min_kbars = recent_ticks["close"].resample("1T").ohlc()

                with self.kline_lock:
                    for timestamp, kline in futures_1min_kbars.iterrows():
                        self.current_kline = {
                            "open": kline["open"],
                            "high": kline["high"],
                            "low": kline["low"],
                            "close": kline["close"],
                            "volume": 0,  # Assume no volume data
                            "start_time": timestamp,
                        }
                        new_kline = pd.DataFrame([self.current_kline])
                        self.kline_df = pd.concat(
                            [self.kline_df, new_kline]
                        ).reset_index(drop=True)

                        # Limit K-line history to the latest 1000 records
                        if len(self.kline_df) > self.max_kline_history:
                            self.kline_df = self.kline_df.iloc[
                                -self.max_kline_history :
                            ]

                        for subscriber in self.kline_subscribers:
                            asyncio.run_coroutine_threadsafe(
                                subscriber(self.current_kline), self.event_loop
                            )

            self.tick_data_list = deque(
                [
                    tick
                    for tick in self.tick_data_list
                    if pd.to_datetime(tick["datetime"], unit="ms").to_datetime64()
                    >= current_minute_start
                ]
            )

        except Exception as e:
            logger.error(f"Error in update_and_publish_kline: {e}")

    def _handle_thread_result(self, future):
        try:
            future.result()
        except Exception as e:
            logger.error(f"Thread processing error: {e}")

    def get_latest_data(self):
        with self.kline_lock:
            return {
                "latest_tick": self.latest_tick,
                "latest_kline": (
                    self.kline_df.iloc[-1] if not self.kline_df.empty else None
                ),
                "kline_history": self.kline_df,
                "latest_orderbook": self.latest_orderbook,
                "orderbook_history": list(self.orderbook_history),
            }


async def main():
    tick_queue = queue.Queue()
    orderbook_queue = queue.Queue()
    event_loop = asyncio.get_event_loop()

    data_processor = DataProcessor(event_loop, tick_queue, orderbook_queue)
    data_publisher = DataPublisher(tick_queue, orderbook_queue)

    # 使用 asyncio.run_coroutine_threadsafe 啟動數據發佈器
    threading.Thread(
        target=lambda: asyncio.run_coroutine_threadsafe(
            data_publisher.start(), event_loop
        ),
        daemon=True,
    ).start()

    # Process both ticks and order books in separate threads
    threading.Thread(target=data_processor.process_ticks, daemon=True).start()
    threading.Thread(target=data_processor.process_orderbooks, daemon=True).start()

    while True:
        data = data_processor.get_latest_data()
        await asyncio.sleep(1)


if __name__ == "__main__":
    asyncio.run(main())

使用 Multiprocessing Pool 和 Thread 的範例

import multiprocessing
import threading
import time


def generate_data(data, num_elements=10):
    for i in range(num_elements):
        data.append(i)
        time.sleep(0.5)  # 模擬生成數據的延遲


def square(x):
    print(
        f"square PID: {multiprocessing.current_process().pid}, TID: {threading.current_thread().ident}"
    )
    return x * x


def main():
    data = []
    num_elements = 10

    # 創建一個線程來生成數據
    data_thread = threading.Thread(target=generate_data, args=(data, num_elements))
    data_thread.start()

    # 創建一個進程池
    pool = multiprocessing.Pool()

    # 獲取進程池中各個進程的 PID
    pool_pids = [p.pid for p in pool._pool]
    print(f"Pool PIDs: {pool_pids}")

    while True:
        print(
            f"main PID: {multiprocessing.current_process().pid}, TID: {threading.current_thread().ident}"
        )
        # 如果數據生成完成,退出循環
        if len(data) == num_elements:
            break
        # 當前數據量
        current_data = data[:]
        # 使用 pool.map 計算當前數據
        if current_data:
            results = pool.map(square, current_data)
            print(f"Current data: {current_data}, Squared results: {results}")
        time.sleep(1)  # 等待一段時間以便數據生成

    # 等待數據生成線程結束
    data_thread.join()

    # 處理最終數據
    results = pool.map(square, data)
    print(f"Final data: {data}, Final squared results: {results}")

    # 關閉進程池
    pool.close()
    pool.join()


if __name__ == "__main__":
    main()

Numba 可以用於加速一些涉及 DataFrame 的操作

import pandas as pd
import numpy as np
from numba import njit

# 创建一个示例 DataFrame
df = pd.DataFrame({
    'A': np.random.rand(1000),
    'B': np.random.rand(1000)
})

@njit
def calculate_sum(A, B):
    result = np.empty(A.shape[0])
    for i in range(A.shape[0]):
        result[i] = A[i] + B[i]
    return result

# 将 DataFrame 转换为 NumPy 数组
A = df['A'].values
B = df['B'].values

# 使用 Numba 加速计算
df['C'] = calculate_sum(A, B)

print(df.head())

指定 CPU 跑在特定核心上運行

import os
import psutil

# 取得當前進程的 PID
pid = os.getpid()
p = psutil.Process(pid)

# 設置程序只運行在 CPU 核心 1(第二核)
p.cpu_affinity([1])

# 驗證當前的 CPU 親和性設定
print(f"CPU 親和性設定為: {p.cpu_affinity()}")

# 模擬負載,觀察運行情況
while True:
    pass  # 佔用 CPU,方便觀察


talib pandas_ta

import pandas as pd
import talib
import pandas_ta as ta

# 模擬較多的 K 線數據
data = {
    "high": [130, 132, 131, 133, 135, 136, 138, 140, 142, 145, 147, 149, 151, 153, 155],
    "low": [125, 126, 128, 130, 132, 133, 134, 137, 139, 141, 143, 145, 146, 148, 150],
    "close": [
        128,
        129,
        130,
        132,
        134,
        135,
        137,
        139,
        141,
        143,
        145,
        147,
        149,
        151,
        153,
    ],
}

df = pd.DataFrame(data)

# 設定 ATR 長度
atr_len = 5

# 使用 talib 計算 TR 和 ATR
df["TR_talib"] = talib.TRANGE(df["high"], df["low"], df["close"])
df["ATR_talib"] = talib.EMA(df["TR_talib"], timeperiod=atr_len)

# 使用 pandas_ta 計算 TR 和 ATR
df["ATR_pandas_ta"] = ta.atr(df["high"], df["low"], df["close"], length=1)
df["ATR_pandas_ta_ema"] = ta.ema(df["ATR_pandas_ta"], length=atr_len)

# 輸出結果
print("DataFrame 結果:")
print(df)

富邦 API FubonSDK

docker pull python:3.10-slim
docker run -it --name my_python_env python:3.10-slim bash
apt update && apt install -y vim unzip wget

wget https://www.fbs.com.tw/TradeAPI_SDK/fubon_binary/fubon_neo-1.3.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.zip
unzip fubon_neo-1.3.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.zip
pip install fubon_neo-1.3.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
pip install requests
from fubon_neo.sdk import FubonSDK
sdk = FubonSDK() 
python -m venv myenv
source myenv/bin/activate
pip install fubon_neo-1.3.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Python 工作時間監控與自動進程重啟系統

import threading
import time
import datetime
import os
import sys
import logging
import ctypes
import subprocess

# 設置日誌
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - [PID:%(process)d][TID:%(thread)d] - %(message)s",
)
logger = logging.getLogger(__name__)


def get_thread_id():
    """獲取當前線程ID"""
    if hasattr(threading, "get_native_id"):  # Python 3.8+
        return threading.get_native_id()
    elif sys.platform == "win32":
        return ctypes.windll.kernel32.GetCurrentThreadId()
    else:
        # Linux/Unix系統通過調用syscall獲取
        try:
            import ctypes

            libc = ctypes.cdll.LoadLibrary("libc.so.6")
            return libc.syscall(186)  # SYS_gettid
        except:
            return os.getpid()  # 備用方案


def is_work_time():
    """檢查當前是否在工作時間內(星期一到星期五,8:20-13:31)"""
    now = datetime.datetime.now()
    # 獲取星期幾 (0=星期一, 6=星期日)
    weekday = now.weekday()

    # 檢查是否為工作日(星期一到星期五)
    if weekday >= 5:  # 星期六或星期日
        return False

    # 獲取當前時間
    current_time = now.time()
    work_start = datetime.time(8, 20)
    work_end = datetime.time(13, 31)

    # 檢查是否在工作時間內
    return work_start <= current_time <= work_end


def restart_program():
    """重新啟動當前程式(保留相同的PID)"""
    logger.info(f"重新啟動程式 (PID: {os.getpid()} 將保持不變)")
    python = sys.executable
    os.execl(python, python, *sys.argv)


def restart_with_new_pid():
    """用新的 PID 重啟程序"""
    logger.info(f"用新 PID 重啟程序 (當前 PID: {os.getpid()})")
    python = sys.executable
    subprocess.Popen([python] + sys.argv)
    sys.exit(0)  # 終止當前進程


def monitoring_thread(use_new_pid=False):
    """監控時間並在非工作時間重啟程式"""
    tid = get_thread_id()
    logger.info(f"時間監控線程已啟動 (TID: {tid})")

    while True:
        if not is_work_time():
            logger.info(f"當前時間不在工作時間範圍內 (TID: {tid})")
            time.sleep(5)  # 延遲5秒後重啟

            if use_new_pid:
                restart_with_new_pid()  # 使用新的PID重啟
            else:
                restart_program()  # 使用原有PID重啟

        # 每分鐘檢查一次
        time.sleep(60)


def main():
    """主程式"""
    pid = os.getpid()
    tid = get_thread_id()
    logger.info(f"\n\n程式啟動 (PID: {pid}, 主線程 TID: {tid})")

    # 決定是否使用新PID重啟
    use_new_pid = True  # 設為True可以使用新PID重啟

    # 啟動監控線程
    monitor = threading.Thread(
        target=monitoring_thread, args=(use_new_pid,), daemon=True
    )
    monitor.start()
    logger.info(f"監控線程 ID: {monitor.ident}")

    # 這裡放置您的主要程式邏輯
    try:
        while True:
            # 您的程式主邏輯
            logger.info(f"主程式運行中... (PID: {pid}, TID: {tid})")

            # 添加更多的主程式邏輯...

            time.sleep(300)  # 示例:每5分鐘執行一次某些任務
    except KeyboardInterrupt:
        logger.info(f"程式被使用者中斷 (PID: {pid}, TID: {tid})")
        sys.exit(0)


if __name__ == "__main__":
    main()

使用 multiprocessing 進行 fork 並殺死主行程的範例


import multiprocessing
import os
import time
import sys

def 子行程():
    """子行程要執行的任務"""
    print(f"新行程啟動,行程ID: {os.getpid()}")
    while True:
        print("子行程正在執行...")
        time.sleep(5)

def 主程式():
    # 建立上下文
    ctx = multiprocessing.get_context('fork')
    
    # 建立新行程
    子行程實例 = ctx.Process(target=子行程)
    子行程實例.start()
    
    print(f"主行程ID: {os.getpid()}")
    print(f"新行程ID: {子行程實例.pid}")
    
    # 等待一段時間
    time.sleep(3)
    
    # 嘗試殺死主行程
    try:
        # 在某些系統可能需要管理員權限
        os.kill(os.getpid(), 9)  # 9 對應 SIGKILL
    except Exception as e:
        print(f"殺死行程出錯: {e}")
        sys.exit()

if __name__ == "__main__":
    主程式()

mermaid

  • https://mermaid-js.github.io/mermaid/#/
  • https://mermaid-js.github.io/mermaid-live-editor/
  • https://github.com/mermaid-js/mermaid

Flowchart

flowchart LR
A[Hard] -->|Text| B(Round)
B --> C{Decision}
C -->|One| D[Result 1]
C -->|Two| E[Result 2]

Sequence diagram

sequenceDiagram
Alice->>John: Hello John, how are you?
loop Healthcheck
    John->>John: Fight against hypochondria
end
Note right of John: Rational thoughts!
John-->>Alice: Great!
John->>Bob: How about you?
Bob-->>John: Jolly good!

Class diagram

classDiagram
Class01 <|-- AveryLongClass : Cool
<<Interface>> Class01
Class09 --> C2 : Where am i?
Class09 --* C3
Class09 --|> Class07
Class07 : equals()
Class07 : Object[] elementData
Class01 : size()
Class01 : int chimp
Class01 : int gorilla
class Class10 {
  <<service>>
  int id
  size()
}

State diagram

stateDiagram-v2
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]

Pie chart

pie
"Dogs" : 386
"Cats" : 85
"Rats" : 15

User Journey diagram

  journey
    title My working day
    section Go to work
      Make tea: 5: Me
      Go upstairs: 3: Me
      Do work: 1: Me, Cat
    section Go home
      Go downstairs: 5: Me
      Sit down: 3: Me

Gantt chart

gantt
    section Section
    Completed :done,    des1, 2014-01-06,2014-01-08
    Active        :active,  des2, 2014-01-07, 3d
    Parallel 1   :         des3, after des1, 1d
    Parallel 2   :         des4, after des1, 1d
    Parallel 3   :         des5, after des3, 1d
    Parallel 4   :         des6, after des4, 1d

1天玩轉c++ socket通信技術

socket是什麼意思 在計算機通信領域,socket 被翻譯為“套接字”,它是計算機之間進行通信的一種約定或一種方式。通過 socket 這種約定,一臺計算機可以接收其他計算機的數據,也可以向其他計算機發送數據。

socket 的典型應用就是 Web 服務器和瀏覽器:瀏覽器獲取用戶輸入的URL,向服務器發起請求,服務器分析接收到的URL,將對應的網頁內容返回給瀏覽器,瀏覽器再經過解析和渲染,就將文字、圖片、視頻等元素呈現給用戶。

學習 socket,也就是學習計算機之間如何通信,並編寫出實用的程序。

IP地址(IP Address)

計算機分佈在世界各地,要想和它們通信,必須要知道確切的位置。確定計算機位置的方式有多種,IP 地址是最常用的,例如,114.114.114.114 是國內第一個、全球第三個開放的 DNS 服務地址,127.0.0.1 是本機地址。

其實,我們的計算機並不知道 IP 地址對應的地理位置,當要通信時,只是將 IP 地址封裝到要發送的數據包中,交給路由器去處理。路由器有非常智能和高效的算法,很快就會找到目標計算機,並將數據包傳遞給它,完成一次單向通信。

目前大部分軟件使用 IPv4 地址,但 IPv6 也正在被人們接受,尤其是在教育網中,已經大量使用。 端口(Port)

有了 IP 地址,雖然可以找到目標計算機,但仍然不能進行通信。一臺計算機可以同時提供多種網絡服務,例如Web服務、FTP服務(文件傳輸服務)、SMTP服務(郵箱服務)等,僅有 IP 地址,計算機雖然可以正確接收到數據包,但是卻不知道要將數據包交給哪個網絡程序來處理,所以通信失敗。

為了區分不同的網絡程序,計算機會為每個網絡程序分配一個獨一無二的端口號(Port Number),例如,Web服務的端口號是 80,FTP 服務的端口號是 21,SMTP 服務的端口號是 25。

**端口(Port)是一個虛擬的、**邏輯上的概念。可以將端口理解為一道門,數據通過這道門流入流出,每道門有不同的編號,就是端口號。如下圖所示:

協議(Protocol)

協議(Protocol)就是網絡通信的約定,通信的雙方必須都遵守才能正常收發數據。協議有很多種,例如 TCP、UDP、IP 等,通信的雙方必須使用同一協議才能通信。協議是一種規範,由計算機組織制定,規定了很多細節,例如,如何建立連接,如何相互識別等。 協議僅僅是一種規範,必須由計算機軟件來實現。例如 IP 協議規定了如何找到目標計算機,那麼各個開發商在開發自己的軟件時就必須遵守該協議,不能另起爐灶。 所謂協議族(Protocol Family),就是一組協議(多個協議)的統稱。最常用的是 TCP/IP 協議族,它包含了 TCP、IP、UDP、Telnet、FTP、SMTP 等上百個互為關聯的協議,由於 TCP、IP 是兩種常用的底層協議,所以把它們統稱為 TCP/IP 協議族。 數據傳輸方式

計算機之間有很多數據傳輸方式,各有優缺點,常用的有兩種:SOCK_STREAM 和 SOCK_DGRAM。

  1. SOCK_STREAM 表示面向連接的數據傳輸方式。數據可以準確無誤地到達另一臺計算機,如果損壞或丟失,可以重新發送,但效率相對較慢。常見的 http 協議就使用 SOCK_STREAM 傳輸數據,因為要確保數據的正確性,否則網頁不能正常解析。

  2. SOCK_DGRAM 表示無連接的數據傳輸方式。計算機只管傳輸數據,不作數據校驗,如果數據在傳輸中損壞,或者沒有到達另一臺計算機,是沒有辦法補救的。也就是說,數據錯了就錯了,無法重傳。因為 SOCK_DGRAM 所做的校驗工作少,所以效率比 SOCK_STREAM 高。

QQ 視頻聊天和語音聊天就使用 SOCK_DGRAM 傳輸數據,因為首先要保證通信的效率,儘量減小延遲,而數據的正確性是次要的,即使丟失很小的一部分數據,視頻和音頻也可以正常解析,最多出現噪點或雜音,不會對通信質量有實質的影響。 注意:SOCK_DGRAM 沒有想象中的糟糕,不會頻繁的丟失數據,數據錯誤只是小概率事件。 有可能多種協議使用同一種數據傳輸方式,所以在 socket 編程中,需要同時指明數據傳輸方式和協議。

綜上所述:IP地址和端口能夠在廣袤的互聯網中定位到要通信的程序,協議和數據傳輸方式規定了如何傳輸數據,有了這些,兩臺計算機就可以通信了。

網路程式設計就是編寫程序使兩臺聯網的電腦相互交換資料。這就是全部內容了嗎?是的!網路程式設計要比想像中的簡單許多。

那麼,這兩臺電腦之間用什麼傳輸資料呢?首先需要物理連接。如今大部分電腦都已經連接到網際網路,因此不用擔心這一點。

在此基礎上,只需要考慮如何編寫資料傳輸程序。但實際上這點也不用愁,因為作業系統已經提供了 socket。即使對網路資料傳輸的原理不太熟悉,我們也能通過 socket 來程式設計。

什麼是 socket?

socket 的原意是“插座”,在電腦通訊領域,socket 被翻譯為“套接字”,它是電腦之間進行通訊的一種約定或一種方式。通過 socket 這種約定,一臺電腦可以接收其他電腦的資料,也可以向其他電腦傳送資料。

我們把插頭插到插座上就能從電網獲得電力供應,同樣,為了與遠端電腦進行資料傳輸,需要連接到網際網路,而 socket 就是用來連接到網際網路的工具。

socket是什麼?

socket 的典型應用就是 Web 伺服器和瀏覽器:瀏覽器獲取使用者輸入的 URL,向伺服器發起請求,伺服器分析接收到的 URL,將對應的網頁內容返回給瀏覽器,瀏覽器再經過解析和渲染,就將文字、圖片、視訊等元素呈現給使用者。

學習 socket,也就是學習電腦之間如何通訊,並編寫出實用的程序。

UNIX/Linux 中的 socket 是什麼?

在 UNIX/Linux 系統中,為了統一對各種硬體的操作,簡化介面,不同的硬體裝置也都被看成一個檔案。對這些檔案的操作,等同於對磁碟上普通檔案的操作。

你也許聽很多高手說過,UNIX/Linux 中的一切都是檔案!那個傢伙說的沒錯。

為了表示和區分已經打開的檔案,UNIX/Linux 會給每個檔案分配一個 ID,這個 ID 就是一個整數,被稱為檔案描述符(File Descriptor)。例如:

  • 通常用 0 來表示標準輸入檔案(stdin),它對應的硬體裝置就是鍵盤;
  • 通常用 1 來表示標準輸出檔案(stdout),它對應的硬體裝置就是顯示器。

UNIX/Linux 程序在執行任何形式的 I/O 操作時,都是在讀取或者寫入一個檔案描述符。一個檔案描述符只是一個和打開的檔案相關聯的整數,它的背後可能是一個硬碟上的普通檔案、FIFO、管道、終端、鍵盤、顯示器,甚至是一個網路連線。

請注意,網路連線也是一個檔案,它也有檔案描述符!你必須理解這句話。

我們可以通過 socket() 函數來建立一個網路連線,或者說打開一個網路檔案,socket() 的返回值就是檔案描述符。有了檔案描述符,我們就可以使用普通的檔案操作函數來傳輸資料了,例如:

  • 用 read() 讀取從遠端電腦傳來的資料;
  • 用 write() 向遠端電腦寫入資料。

你看,只要用 socket() 建立了連接,剩下的就是檔案操作了,網路程式設計原來就是如此簡單!

Window 系統中的 socket 是什麼?

Windows 也有類似“檔案描述符”的概念,但通常被稱為“檔案控制代碼”。因此,本教學如果涉及 Windows 平臺將使用“控制代碼”,如果涉及 Linux 平臺則使用“描述符”。

與 UNIX/Linux 不同的是,Windows 會區分 socket 和檔案,Windows 就把 socket 當做一個網路連線來對待,因此需要呼叫專門針對 socket 而設計的資料傳輸函數,針對普通檔案的輸入輸出函數就無效了。

套接字有哪些類型?socket有哪些類型?

這個世界上有很多種套接字(socket),比如 DARPA Internet 地址(Internet 套接字)、本地節點的路徑名(Unix套接字)、CCITT X.25地址(X.25 套接字)等。但本教學只講第一種套接字——Internet 套接字,它是最具代表性的,也是最經典最常用的。以後我們提及套接字,指的都是 Internet 套接字。

根據資料的傳輸方式,可以將 Internet 套接字分成兩種類型。通過 socket() 函數建立連接時,必須告訴它使用哪種資料傳輸方式。

Internet 套接字其實還有很多其它資料傳輸方式,但是我可不想嚇到你,本教學只講常用的兩種。

流格式套接字(SOCK_STREAM)

流格式套接字(Stream Sockets)也叫“面向連接的套接字”,在程式碼中使用 SOCK_STREAM 表示。

SOCK_STREAM 是一種可靠的、雙向的通訊資料流,資料可以精準無誤地到達另一臺電腦,如果損壞或丟失,可以重新傳送。

流格式套接字有自己的糾錯機制,在此我們就不討論了。

SOCK_STREAM 有以下幾個特徵:

  • 資料在傳輸過程中不會消失;
  • 資料是按照順序傳輸的;
  • 資料的傳送和接收不是同步的(有的教學也稱“不存在資料邊界”)。

可以將 SOCK_STREAM 比喻成一條傳送帶,只要傳送帶本身沒有問題(不會斷網),就能保證資料不丟失;同時,較晚傳送的資料不會先到達,較早傳送的資料不會晚到達,這就保證了資料是按照順序傳遞的。

將面向連接的套接字比喻成傳送帶

為什麼流格式套接字可以達到高品質的資料傳輸呢?這是因為它使用了 TCP 協議(The Transmission Control Protocol,傳輸控制協議),TCP 協議會控制你的資料按照順序到達並且沒有錯誤。

你也許見過 TCP,是因為你經常聽說“TCP/IP”。TCP 用來確保資料的正確性,IP(Internet Protocol,網路協議)用來控制資料如何從源頭到達目的地,也就是常說的“路由”。

那麼,“資料的傳送和接收不同步”該如何理解呢?

假設傳送帶傳送的是水果,接收者需要湊齊 100 個後才能裝袋,但是傳送帶可能把這 100 個水果分批傳送,比如第一批傳送 20 個,第二批傳送 50 個,第三批傳送 30 個。接收者不需要和傳送帶保持同步,只要根據自己的節奏來裝袋即可,不用管傳送帶傳送了幾批,也不用每到一批就裝袋一次,可以等到湊夠了 100 個水果再裝袋。

流格式套接字的內部有一個緩衝區(也就是字元陣列),通過 socket 傳輸的資料將保存到這個緩衝區。接收端在收到資料後並不一定立即讀取,只要資料不超過緩衝區的容量,接收端有可能在緩衝區被填滿以後一次性地讀取,也可能分成好幾次讀取。

也就是說,不管資料分幾次傳送過來,接收端只需要根據自己的要求讀取,不用非得在資料到達時立即讀取。傳送端有自己的節奏,接收端也有自己的節奏,它們是不一致的。

流格式套接字有什麼實際的應用場景嗎?瀏覽器所使用的 http 協議就基於面向連接的套接字,因為必須要確保資料精準無誤,否則載入的 HTML 將無法解析。

資料報格式套接字(SOCK_DGRAM)

資料報格式套接字(Datagram Sockets)也叫“無連接的套接字”,在程式碼中使用 SOCK_DGRAM 表示。

電腦只管傳輸資料,不作資料校驗,如果資料在傳輸中損壞,或者沒有到達另一臺電腦,是沒有辦法補救的。也就是說,資料錯了就錯了,無法重傳。

因為資料報套接字所做的校驗工作少,所以在傳輸效率方面比流格式套接字要高。

可以將 SOCK_DGRAM 比喻成高速移動的摩托車快遞,它有以下特徵:

  • 強調快速傳輸而非傳輸順序;
  • 傳輸的資料可能丟失也可能損毀;
  • 限制每次傳輸的資料大小;
  • 資料的傳送和接收是同步的(有的教學也稱“存在資料邊界”)。

眾所周知,速度是快遞行業的生命。用摩托車發往同一地點的兩件包裹無需保證順序,只要以最快的速度交給客戶就行。這種方式存在損壞或丟失的風險,而且包裹大小有一定限制。因此,想要傳遞大量包裹,就得分配傳送。

將無連接套接字比喻成摩托車快遞

另外,用兩輛摩托車分別傳送兩件包裹,那麼接收者也需要分兩次接收,所以“資料的傳送和接收是同步的”;換句話說,接收次數應該和傳送次數相同。

總之,資料報套接字是一種不可靠的、不按順序傳遞的、以追求速度為目的的套接字。

資料報套接字也使用 IP 協議作路由,但是它不使用 TCP 協議,而是使用 UDP 協議(User Datagram Protocol,使用者資料報協議)。

QQ 視訊聊天和語音聊天就使用 SOCK_DGRAM 來傳輸資料,因為首先要保證通訊的效率,儘量減小延遲,而資料的正確性是次要的,即使丟失很小的一部分資料,視訊和音訊也可以正常解析,最多出現噪點或雜音,不會對通訊質量有實質的影響。

OSI網路七層模型

如果你讀過電腦專業,或者學習過網路通訊,那你一定聽說過 OSI 模型,它曾無數次讓你頭大。OSI 是 Open System Interconnection 的縮寫,譯為“開放式系統互聯”。

OSI 模型把網路通訊的工作分為 7 層,從下到上分別是物理層、資料鏈路層、網路層、傳輸層、會話層、表示層和應用層。

OSI 只是存在於概念和理論上的一種模型,它的缺點是分層太多,增加了網路工作的複雜性,所以沒有大規模應用。後來人們對 OSI 進行了簡化,合併了一些層,最終只保留了 4 層,從下到上分別是介面層、網路層、傳輸層和應用層,這就是大名鼎鼎的 TCP/IP 模型。

OSI 七層網路模型和 TCP/IP 四層網路模型的對比 圖1:OSI 七層網路模型和 TCP/IP 四層網路模型的對比

這個網路模型究竟是幹什麼呢?簡而言之就是進行資料封裝的。

我們平常使用的程序(或者說軟體)一般都是通過應用層來訪問網路的,程序產生的資料會一層一層地往下傳輸,直到最後的網路介面層,就通過網線傳送到網際網路上去了。資料每往下走一層,就會被這一層的協議增加一層包裝,等到傳送到網際網路上時,已經比原始資料多了四層包裝。整個資料封裝的過程就像俄羅斯套娃。

當另一臺電腦接收到封包時,會從網路介面層再一層一層往上傳輸,每傳輸一層就拆開一層包裝,直到最後的應用層,就得到了最原始的資料,這才是程序要使用的資料。

給資料加包裝的過程,實際上就是在資料的頭部增加一個標誌(一個資料區塊),表示資料經過了這一層,我已經處理過了。給資料拆包裝的過程正好相反,就是去掉資料頭部的標誌,讓它逐漸現出原形。

你看,在網際網路上傳輸一份資料是多麼地複雜啊,而我們卻感受不到,這就是網路模型的厲害之處。我們只需要在程式碼中呼叫一個函數,就能讓下面的所有網路層為我們工作。

我們所說的 socket 程式設計,是站在傳輸層的基礎上,所以可以使用 TCP/UDP 協議,但是不能幹「訪問網頁」這樣的事情,因為訪問網頁所需要的 http 協議位於應用層。

兩臺電腦進行通訊時,必須遵守以下原則:

  • 必須是同一層次進行通訊,比如,A 電腦的應用層和 B 電腦的傳輸層就不能通訊,因為它們不在一個層次,資料的拆包會遇到問題。
  • 每一層的功能都必須相同,也就是擁有完全相同的網路模型。如果網路模型都不同,那不就亂套了,誰都不認識誰。
  • 資料只能逐層傳輸,不能躍層。
  • 每一層可以使用下層提供的服務,並向上層提供服務。

TCP/IP協議族

上節《OSI網路七層模型》中講到,目前實際使用的網路模型是 TCP/IP 模型,它對 OSI 模型進行了簡化,只包含了四層,從上到下分別是應用層、傳輸層、網路層和鏈路層(網路介面層),每一層都包含了若干協議。

協議(Protocol)就是網路通訊過程中的約定或者合同,通訊的雙方必須都遵守才能正常收發資料。協議有很多種,例如 TCP、UDP、IP 等,通訊的雙方必須使用同一協議才能通訊。協議是一種規範,由電腦組織制定,規定了很多細節,例如,如何建立連接,如何相互識別等。

協議僅僅是一種規範,必須由電腦軟體來實現。例如 IP 協議規定了如何找到目標電腦,那麼各個開發商在開發自己的軟體時就必須遵守該協議,不能另起爐灶。

TCP/IP 模型包含了 TCP、IP、UDP、Telnet、FTP、SMTP 等上百個互為關聯的協議,其中 TCP 和 IP 是最常用的兩種底層協議,所以把它們統稱為“TCP/IP 協議族”。

也就是說,“TCP/IP模型”中所涉及到的協議稱為“TCP/IP協議族”,你可以區分這兩個概念,也可以認為它們是等價的,隨便你怎麼想。

本教學所講的 socket 程式設計是基於 TCP 和 UDP 協議的,它們的層級關係如下圖所示:

TCP/IP協議的層次圖

【擴展閱讀】開放式系統(Open System)

把協議分成多個層次有哪些優點?協議設計更容易?當然這也足以成為優點之一。但是還有更重要的原因,就是為了通過標準化操作設計成開放式系統。

標準本身就是對外公開的,會引導更多的人遵守規範。以多個標準為依據設計的系統稱為開放式系統(Open System),我們現在學習的 TCP/IP 協議族也屬於其中之一。

接下來瞭解一下開放式系統具有哪些優點。

路由器用來完成 IP 層的互動任務。某個網路原來使用 A 公司的路由器,現要將其替換成 B 公司的,是否可行?這並非難事,並不一定要換成同一公司的同一型號路由器,因為所有生產商都會按照 IP 層標準製造。

再舉個例子。大家的電腦是否裝有網路介面卡,也就是所謂的網路卡?尚未安裝也無妨,其實很容易買到,因為所有網路卡製造商都會遵守鏈路層的協議標準。這就是開放式系統的優點。

標準的存在意味著高速的技術發展,這也是開放式系統設計最大的原因所在。實際上,軟體工程中的“物件導向(Object Oriented)”的誕生背景中也有標準化的影子。也就是說,標準對於技術發展起著舉足輕重的作用。

IP、MAC和連接埠號——網路通訊中確認身份資訊的三要素

在茫茫的網際網路海洋中,要找到一臺電腦非常不容易,有三個要素必須具備,它們分別是 IP 地址、MAC 地址和連接埠號。

IP地址

IP地址是 Internet Protocol Address 的縮寫,譯為“網際協議地址”。

目前大部分軟體使用 IPv4 地址,但 IPv6 也正在被人們接受,尤其是在教育網中,已經大量使用。

一臺電腦可以擁有一個獨立的 IP 地址,一個區域網路也可以擁有一個獨立的 IP 地址(對外就好像只有一臺電腦)。對於目前廣泛使用 IPv4 地址,它的資源是非常有限的,一臺電腦一個 IP 地址是不現實的,往往是一個區域網路才擁有一個 IP 地址。

在網際網路上進行通訊時,必須要知道對方的 IP 地址。實際上封包中已經附帶了 IP 地址,把封包傳送給路由器以後,路由器會根據 IP 地址找到對方的地裡位置,完成一次資料的傳遞。路由器有非常高效和智能的演算法,很快就會找到目標電腦。

MAC地址

現實的情況是,一個區域網路往往才能擁有一個獨立的 IP;換句話說,IP 地址只能定位到一個區域網路,無法定位到具體的一臺電腦。這可怎麼辦呀?這樣也沒法通訊啊。

其實,真正能唯一標識一臺電腦的是 MAC 地址,每個網路卡的 MAC 地址在全世界都是獨一無二的。電腦出廠時,MAC 地址已經被寫死到網路卡裡面了(當然通過某些“奇巧淫技”也是可以修改的)。區域網路中的路由器/交換機會記錄每臺電腦的 MAC 地址。

MAC 地址是 Media Access Control Address 的縮寫,直譯為“媒體存取控制地址”,也稱為區域網路地址(LAN Address),乙太網路地址(Ethernet Address)或實體位址(Physical Address)。

封包中除了會附帶對方的 IP 地址,還會附帶對方的 MAC 地址,當封包達到區域網路以後,路由器/交換機會根據封包中的 MAC 地址找到對應的電腦,然後把封包轉交給它,這樣就完成了資料的傳遞。

連接埠號

有了 IP 地址和 MAC 地址,雖然可以找到目標電腦,但仍然不能進行通訊。一臺電腦可以同時提供多種網路服務,例如 Web 服務(網站)、FTP 服務(檔案傳輸服務)、SMTP 服務(信箱服務)等,僅有 IP 地址和 MAC 地址,電腦雖然可以正確接收到封包,但是卻不知道要將封包交給哪個網路程序來處理,所以通訊失敗。

為了區分不同的網路程序,電腦會為每個網路程序分配一個獨一無二的連接埠號(Port Number),例如,Web 服務的連接埠號是 80,FTP 服務的連接埠號是 21,SMTP 服務的連接埠號是 25。

連接埠(Port)是一個虛擬的、邏輯上的概念。可以將連接埠理解為一道門,資料通過這道門流入流出,每道門有不同的編號,就是連接埠號。如下圖所示:

img

Linux下的socket演示程序

我們從一個簡單的“Hello World!”程序切入 socket 程式設計。

本節演示了 Linux 下的程式碼,server.cpp 是伺服器端程式碼,client.cpp 是客戶端程式碼,要實現的功能是:客戶端從伺服器讀取一個字串並列印出來。

伺服器端程式碼 server.cpp:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main()
{
    //建立套接字
    int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    //將套接字和IP、連接埠繫結
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每個位元組都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具體的IP地址
    serv_addr.sin_port = htons(1234);  //連接埠
    bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    //進入監聽狀態,等待使用者發起請求
    listen(serv_sock, 20);

    //接收客戶端請求
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr,
                           &clnt_addr_size);

    //向客戶端傳送資料
    char str[] = "http://c.biancheng.net/socket/";
    write(clnt_sock, str, sizeof(str));

    //關閉套接字
    close(clnt_sock);
    close(serv_sock);

    return 0;
}

客戶端程式碼 client.cpp:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main()
{
    //建立套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);

    //向伺服器(特定的IP和連接埠)發起請求
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每個位元組都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具體的IP地址
    serv_addr.sin_port = htons(1234);  //連接埠
    connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    //讀取伺服器傳回的資料
    char buffer[40];
    read(sock, buffer, sizeof(buffer) - 1);

    printf("Message form server: %s\n", buffer);

    //關閉套接字
    close(sock);

    return 0;
}

啟動一個終端(Shell),先編譯 server.cpp 並運行:

[admin@localhost ~]$ g++ server.cpp -o server [admin@localhost ~]$ ./server #等待請求的到來

正常情況下,程式執行到 accept() 函數就會被阻塞,等待客戶端發起請求。

接下再啟動一個終端,編譯 client.cpp 並運行:

[admin@localhost ~]$ g++ client.cpp -o client [admin@localhost ~]$ ./client Message form server: http://c.biancheng.net/socket/

client 接收到從 server傳送過來的字串就運行結束了,同時,server 完成傳送字串的任務也運行結束了。大家可以通過兩個打開的終端來觀察。

client 運行後,通過 connect() 函數向 server 發起請求,處於監聽狀態的 server 被啟動,執行 accept() 函數,接受客戶端的請求,然後執行 write() 函數向 client 傳回資料。client 接收到傳回的資料後,connect() 就運行結束了,然後使用 read() 將資料讀取出來。

server 只接受一次 client 請求,當 server 向 client 傳回資料後,程序就運行結束了。如果想再次接收到伺服器的資料,必須再次運行 server,所以這是一個非常簡陋的 socket 程序,不能夠一直接受客戶端的請求。

原始碼解析

  • 先說一下 server.cpp 中的程式碼。

    • 第 11 行通過 socket() 函數建立了一個套接字,參數 AF_INET 表示使用 IPv4 地址,SOCK_STREAM 表示使用面向連接的套接字,IPPROTO_TCP 表示使用 TCP 協議。在 Linux 中,socket 也是一種檔案,有檔案描述符,可以使用 write() / read() 函數進行 I/O 操作,這一點已在《socket是什麼》中進行了講解。
    • 第 19 行通過 bind() 函數將套接字 serv_sock 與特定的 IP 地址和連接埠繫結,IP 地址和連接埠都保存在 sockaddr_in 結構體中。
    • socket() 函數確定了套接字的各種屬性,bind() 函數讓套接字與特定的IP地址和連接埠對應起來,這樣客戶端才能連接到該套接字。
    • 第 22 行讓套接字處於被動監聽狀態。所謂被動監聽,是指套接字一直處於“睡眠”中,直到客戶端發起請求才會被“喚醒”。
    • 第 27 行的 accept() 函數用來接收客戶端的請求。程序一旦執行到 accept() 就會被阻塞(暫停運行),直到客戶端發起請求。
    • 第 31 行的 write() 函數用來向套接字檔案中寫入資料,也就是向客戶端傳送資料。
    • 和普通檔案一樣,socket 在使用完畢後也要用 close() 關閉。
  • 再說一下 client.cpp 中的程式碼。client.cpp 中的程式碼和 server.cpp 中有一些區別。

    • 第 19 行程式碼通過 connect() 向伺服器發起請求,伺服器的IP地址和連接埠號保存在 sockaddr_in 結構體中。直到伺服器傳回資料後,connect() 才運行結束。
    • 第 23 行程式碼通過 read() 從套接字檔案中讀取資料。

Windows下的socket演示程序

這節來看一下 Windows 下的 socket 程序。同樣,server.cpp 為伺服器端程式碼,client 為客戶端程式碼。

伺服器端程式碼 server.cpp:

#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")  //載入 ws2_32.dll

int main()
{
    //初始化 DLL
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);

    //建立套接字
    SOCKET servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

    //繫結套接字
    struct sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr));  //每個位元組都用0填充
    sockAddr.sin_family = PF_INET;  //使用IPv4地址
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具體的IP地址
    sockAddr.sin_port = htons(1234);  //連接埠
    bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

    //進入監聽狀態
    listen(servSock, 20);

    //接收客戶端請求
    SOCKADDR clntAddr;
    int nSize = sizeof(SOCKADDR);
    SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);

    //向客戶端傳送資料
    char* str = "Hello World!";
    send(clntSock, str, strlen(str) + sizeof(char), NULL);

    //關閉套接字
    closesocket(clntSock);
    closesocket(servSock);

    //終止 DLL 的使用
    WSACleanup();

    return 0;
}

客戶端程式碼 client.cpp:

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")  //載入 ws2_32.dll

int main()
{
    //初始化DLL
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);

    //建立套接字
    SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

    //向伺服器發起請求
    struct sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr));  //每個位元組都用0填充
    sockAddr.sin_family = PF_INET;
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    sockAddr.sin_port = htons(1234);
    connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

    //接收伺服器傳回的資料
    char szBuffer[MAXBYTE] = {0};
    recv(sock, szBuffer, MAXBYTE, NULL);

    //輸出接收到的資料
    printf("Message form server: %s\n", szBuffer);

    //關閉套接字
    closesocket(sock);

    //終止使用 DLL
    WSACleanup();

    system("pause");
    return 0;
}

將 server.cpp 和 client.cpp 分別編譯為 server.exe 和 client.exe,先運行 server.exe,再運行 client.exe,輸出結果為: Message form server: Hello World!

Windows 下的 socket 程序和 Linux 思路相同,但細節有所差別:

  • Windows 下的 socket 程序依賴 Winsock.dll 或 ws2_32.dll,必須提前載入。DLL 有兩種載入方式,請查看:動態連結庫DLL的載入
  • Linux 使用“檔案描述符”的概念,而 Windows 使用“檔案控制代碼”的概念;Linux 不區分 socket 檔案和普通檔案,而 Windows 區分;Linux 下 socket() 函數的返回值為 int 類型,而 Windows 下為 SOCKET 類型,也就是控制代碼。
  • Linux 下使用 read() / write() 函數讀寫,而 Windows 下使用 recv() / send() 函數傳送和接收。
  • 關閉 socket 時,Linux 使用 close() 函數,而 Windows 使用 closesocket() 函數。

socket()函數用法詳解:建立套接字

不管是 Windows 還是 Linux,都使用 socket() 函數來建立套接字。socket() 在兩個平臺下的參數是相同的,不同的是返回值。

在《socket是什麼》一節中我們講到了 Windows 和 Linux 在對待 socket 方面的區別。

Linux 中的一切都是檔案,每個檔案都有一個整數類型的檔案描述符;socket 也是一個檔案,也有檔案描述符。使用 socket() 函數建立套接字以後,返回值就是一個 int 類型的檔案描述符。

Windows 會區分 socket 和普通檔案,它把 socket 當做一個網路連線來對待,呼叫 socket() 以後,返回值是 SOCKET 類型,用來表示一個套接字。

Linux 下的 socket() 函數

在 Linux 下使用 <sys/socket.h> 標頭檔中 socket() 函數來建立套接字,原型為:

int socket(int af, int type, int protocol);
  • af 為地址族(Address Family),也就是 IP 地址類型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的簡寫,INET是“Inetnet”的簡寫。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。

大家需要記住127.0.0.1,它是一個特殊IP地址,表示本機地址,後面的教學會經常用到。

你也可以使用 PF 前綴,PF 是“Protocol Family”的簡寫,它和 AF 是一樣的。例如,PF_INET 等價於 AF_INET,PF_INET6 等價於 AF_INET6。

  • type 為資料傳輸方式/套接字類型,常用的有 SOCK_STREAM(流格式套接字/面向連接的套接字) 和 SOCK_DGRAM(資料報套接字/無連接的套接字),我們已經在《套接字有哪些類型》一節中進行了介紹。

  • protocol 表示傳輸協議,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分別表示 TCP 傳輸協議和 UDP 傳輸協議。

有了地址類型和資料傳輸方式,還不足以決定採用哪種協議嗎?為什麼還需要第三個參數呢?

正如大家所想,一般情況下有了 af 和 type 兩個參數就可以建立套接字了,作業系統會自動推演出協議類型,除非遇到這樣的情況:有兩種不同的協議支援同一種地址類型和資料傳輸類型。如果我們不指明使用哪種協議,作業系統是沒辦法自動推演的。

本教學使用 IPv4 地址,參數 af 的值為 PF_INET。如果使用 SOCK_STREAM 傳輸資料,那麼滿足這兩個條件的協議只有 TCP,因此可以這樣來呼叫 socket() 函數:

int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  //IPPROTO_TCP表示TCP協議

這種套接字稱為 TCP 套接字。

如果使用 SOCK_DGRAM 傳輸方式,那麼滿足這兩個條件的協議只有 UDP,因此可以這樣來呼叫 socket() 函數:

int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);  //IPPROTO_UDP表示UDP協議

這種套接字稱為 UDP 套接字。

上面兩種情況都只有一種協議滿足條件,可以將 protocol 的值設為 0,系統會自動推演出應該使用什麼協議,如下所示:

int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //建立TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //建立UDP套接字

後面的教學中多採用這種簡化寫法。

在Windows下建立socket

Windows 下也使用 socket() 函數來建立套接字,原型為:

SOCKET socket(int af, int type, int protocol);

除了返回值類型不同,其他都是相同的。Windows 不把套接字作為普通檔案對待,而是返回 SOCKET 類型的控制代碼。請看下面的例子:

SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);  //建立TCP套接字

bind()和connect()函數:繫結套接字並建立連接

socket() 函數用來建立套接字,確定套接字的各種屬性,然後伺服器端要用 bind() 函數將套接字與特定的 IP 地址和連接埠繫結起來,只有這樣,流經該 IP 地址和連接埠的資料才能交給套接字處理。類似地,客戶端也要用 connect() 函數建立連接。

bind() 函數

bind() 函數的原型為:

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);  //Windows

下面以 Linux 為例進行講解,Windows 與此類似。

sock 為 socket 檔案描述符,addr 為 sockaddr 結構體變數的指針,addrlen 為 addr 變數的大小,可由 sizeof() 計算得出。

下面的程式碼,將建立的套接字與IP地址 127.0.0.1、連接埠 1234 繫結:

//建立套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

//建立sockaddr_in結構體變數
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));  //每個位元組都用0填充
serv_addr.sin_family = AF_INET;  //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具體的IP地址
serv_addr.sin_port = htons(1234);  //連接埠

//將套接字和IP、連接埠繫結
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

這裡我們使用 sockaddr_in 結構體,然後再強制轉換為 sockaddr 類型,後邊會講解為什麼這樣做。

sockaddr_in 結構體

接下來不妨先看一下 sockaddr_in 結構體,它的成員變數如下:

struct sockaddr_in {
    sa_family_t
    sin_family;   //地址族(Address Family),也就是地址類型
    uint16_t        sin_port;     //16位的連接埠號
    struct in_addr  sin_addr;     //32位IP地址
    char            sin_zero[8];  //不使用,一般用0填充
};
  • sin_family 和 socket() 的第一個參數的含義相同,取值也要保持一致。

  • sin_prot 為連接埠號。uint16_t 的長度為兩個位元組,理論上連接埠號的取值範圍為 0~65536,但 0~1023 的連接埠一般由系統分配給特定的服務程序,例如 Web 服務的連接埠號為 80,FTP 服務的連接埠號為 21,所以我們的程序要儘量在 1024~65536 之間分配連接埠號。連接埠號需要用 htons() 函數轉換,後面會講解為什麼。

  • sin_addr 是 struct in_addr 結構體類型的變數,下面會詳細講解。

  • sin_zero[8] 是多餘的8個位元組,沒有用,一般使用 memset() 函數填充為 0。上面的程式碼中,先用 memset() 將結構體的全部位元組填充為 0,再給前3個成員賦值,剩下的 sin_zero 自然就是 0 了。

in_addr 結構體

sockaddr_in 的第3個成員是 in_addr 類型的結構體,該結構體只包含一個成員,如下所示:

struct in_addr {
    in_addr_t  s_addr;  //32位的IP地址
};

in_addr_t 在標頭檔 <netinet/in.h> 中定義,等價於 unsigned long,長度為4個位元組。也就是說,s_addr 是一個整數,而IP地址是一個字串,所以需要 inet_addr() 函數進行轉換,例如:

unsigned long ip = inet_addr("127.0.0.1");
printf("%ld\n", ip);

運行結果: 16777343

img 圖解 sockaddr_in 結構體

為什麼要搞這麼複雜,結構體中巢狀結構體,而不用 sockaddr_in 的一個成員變數來指明IP地址呢?socket() 函數的第一個參數已經指明瞭地址類型,為什麼在 sockaddr_in 結構體中還要再說明一次呢,這不是囉嗦嗎?

這些繁瑣的細節確實給初學者帶來了一定的障礙,我想,這或許是歷史原因吧,後面的介面總要相容前面的程式碼。各位讀者一定要有耐心,暫時不理解沒有關係,根據教學中的程式碼“照貓畫虎”即可,時間久了自然會接受。

為什麼使用 sockaddr_in 而不使用 sockaddr

bind() 第二個參數的類型為 sockaddr,而程式碼中卻使用 sockaddr_in,然後再強制轉換為 sockaddr,這是為什麼呢?

struct sockaddr {
    sa_family_t  sin_family;   //地址族(Address Family),也就是地址類型
    char         sa_data[14];  //IP地址和連接埠號
};

下圖是 sockaddr 與 sockaddr_in 的對比(括號中的數字表示所佔用的位元組數): img

sockaddr 和 sockaddr_in 的長度相同,都是16位元組,只是將IP地址和連接埠號合併到一起,用一個成員 sa_data 表示。要想給 sa_data 賦值,必須同時指明IP地址和連接埠號,例如”127.0.0.1:80“,遺憾的是,沒有相關函數將這個字串轉換成需要的形式,也就很難給 sockaddr 類型的變數賦值,所以使用 sockaddr_in 來代替。這兩個結構體的長度相同,強制轉換類型時不會丟失位元組,也沒有多餘的位元組。

可以認為,sockaddr 是一種通用的結構體,可以用來保存多種類型的IP地址和連接埠號,而 sockaddr_in 是專門用來保存 IPv4 地址的結構體。另外還有 sockaddr_in6,用來保存 IPv6 地址,它的定義如下:

struct sockaddr_in6 {
    sa_family_t sin6_family;  //(2)地址類型,取值為AF_INET6
    in_port_t sin6_port;  //(2)16位連接埠號
    uint32_t sin6_flowinfo;  //(4)IPv6流資訊
    struct in6_addr sin6_addr;  //(4)具體的IPv6地址
    uint32_t sin6_scope_id;  //(4)介面範圍ID
};

正是由於通用結構體 sockaddr 使用不便,才針對不同的地址類型定義了不同的結構體。

connect() 函數

connect() 函數用來建立連接,它的原型為:

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);  //Linux
int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen);  //Windows

各個參數的說明和 bind() 相同,不再贅述。

listen()和accept()函數:讓套接字進入監聽狀態並響應客戶端請求

對於伺服器端程序,使用 bind() 繫結套接字後,還需要使用 listen() 函數讓套接字進入被動監聽狀態,再呼叫 accept() 函數,就可以隨時響應客戶端的請求了。

listen() 函數

通過 listen() 函數可以讓套接字進入被動監聽狀態,它的原型為:

int listen(int sock, int backlog);  //Linux
int listen(SOCKET sock, int backlog);  //Windows

sock 為需要進入監聽狀態的套接字,backlog 為請求佇列的最大長度。

所謂被動監聽,是指當沒有客戶端請求時,套接字處於“睡眠”狀態,只有當接收到客戶端請求時,套接字才會被“喚醒”來響應請求。

請求佇列

當套接字正在處理客戶端請求時,如果有新的請求進來,套接字是沒法處理的,只能把它放進緩衝區,待當前請求處理完畢後,再從緩衝區中讀取出來處理。如果不斷有新的請求進來,它們就按照先後順序在緩衝區中排隊,直到緩衝區滿。這個緩衝區,就稱為請求佇列(Request Queue)。

緩衝區的長度(能存放多少個客戶端請求)可以通過 listen() 函數的 backlog 參數指定,但究竟為多少並沒有什麼標準,可以根據你的需求來定,並行量小的話可以是10或者20。

如果將 backlog 的值設定為 SOMAXCONN,就由系統來決定請求佇列長度,這個值一般比較大,可能是幾百,或者更多。

當請求佇列滿時,就不再接收新的請求,對於 Linux,客戶端會收到 ECONNREFUSED 錯誤,對於 Windows,客戶端會收到 WSAECONNREFUSED 錯誤。

注意:listen() 只是讓套接字處於監聽狀態,並沒有接收請求。接收請求需要使用 accept() 函數。

accept() 函數

當套接字處於監聽狀態時,可以通過 accept() 函數來接收客戶端請求。它的原型為:

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);  //Linux
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);  //Windows

它的參數與 listen() 和 connect() 是相同的:sock 為伺服器端套接字,addr 為 sockaddr_in 結構體變數,addrlen 為參數 addr 的長度,可由 sizeof() 求得。

accept() 返回一個新的套接字來和客戶端通訊,addr 保存了客戶端的IP地址和連接埠號,而 sock 是伺服器端的套接字,大家注意區分。後面和客戶端通訊時,要使用這個新生成的套接字,而不是原來伺服器端的套接字。

最後需要說明的是:listen() 只是讓套接字進入監聽狀態,並沒有真正接收客戶端請求,listen() 後面的程式碼會繼續執行,直到遇到 accept()。accept() 會阻塞程序執行(後面程式碼不能被執行),直到有新的請求到來。

send()/recv()和write()/read():傳送資料和接收資料

在 Linux 和 Windows 平臺下,使用不同的函數傳送和接收 socket 資料,下面我們分別講解。

Linux下資料的接收和傳送

Linux 不區分套接字檔案和普通檔案,使用 write() 可以向套接字中寫入資料,使用 read() 可以從套接字中讀取資料。

前面我們說過,兩臺電腦之間的通訊相當於兩個套接字之間的通訊,在伺服器端用 write() 向套接字寫入資料,客戶端就能收到,然後再使用 read() 從套接字中讀取出來,就完成了一次通訊。

write() 的原型為:

ssize_t write(int fd, const void *buf, size_t nbytes);

fd 為要寫入的檔案的描述符,buf 為要寫入的資料的緩衝區地址,nbytes 為要寫入的資料的位元組數。

size_t 是通過 typedef 聲明的 unsigned int 類型;ssize_t 在 "size_t" 前面加了一個"s",代表 signed,即 ssize_t 是通過 typedef 聲明的 signed int 類型。

write() 函數會將緩衝區 buf 中的 nbytes 個位元組寫入檔案 fd,成功則返回寫入的位元組數,失敗則返回 -1。

read() 的原型為:

ssize_t read(int fd, void *buf, size_t nbytes);

fd 為要讀取的檔案的描述符,buf 為要接收資料的緩衝區地址,nbytes 為要讀取的資料的位元組數。

read() 函數會從 fd 檔案中讀取 nbytes 個位元組並保存到緩衝區 buf,成功則返回讀取到的位元組數(但遇到檔案結尾則返回0),失敗則返回 -1。

Windows下資料的接收和傳送

Windows 和 Linux 不同,Windows 區分普通檔案和套接字,並定義了專門的接收和傳送的函數。

從伺服器端傳送資料使用 send() 函數,它的原型為:

int send(SOCKET sock, const char *buf, int len, int flags);

sock 為要傳送資料的套接字,buf 為要傳送的資料的緩衝區地址,len 為要傳送的資料的位元組數,flags 為傳送資料時的選項。

返回值和前三個參數不再贅述,最後的 flags 參數一般設定為 0 或 NULL,初學者不必深究。

在客戶端接收資料使用 recv() 函數,它的原型為:

int recv(SOCKET sock, char *buf, int len, int flags);

使用socket程式設計實現回聲客戶端

所謂“回聲”,是指使用者端向伺服器傳送一條資料,伺服器再將資料原樣返回給使用者端,就像聲音一樣,遇到障礙物會被“反彈回來”。

對!使用者端也可以使用 write() / send() 函數向伺服器傳送資料,伺服器也可以使用 read() / recv() 函數接收資料。

考慮到大部分初學者使用 Windows 作業系統,本節將實現 Windows 下的回聲程式,Linux 下稍作修改即可,不再給出程式碼。

伺服器端 server.cpp:

#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")  //載入 ws2_32.dll

#define BUF_SIZE 100

int main(){
    WSADATA wsaData;
    WSAStartup( MAKEWORD(2, 2), &wsaData);

    //建立通訊端
    SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);

    //系結通訊端
    sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr));  //每個位元組都用0填充
    sockAddr.sin_family = PF_INET;  //使用IPv4地址
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具體的IP地址
    sockAddr.sin_port = htons(1234);  //埠
    bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

    //進入監聽狀態
    listen(servSock, 20);

    //接收使用者端請求
    SOCKADDR clntAddr;
    int nSize = sizeof(SOCKADDR);
    SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
    char buffer[BUF_SIZE];  //緩衝區
    int strLen = recv(clntSock, buffer, BUF_SIZE, 0);  //接收使用者端發來的資料
    send(clntSock, buffer, strLen, 0);  //將資料原樣返回

    //關閉通訊端
    closesocket(clntSock);
    closesocket(servSock);

    //終止 DLL 的使用
    WSACleanup();

    return 0;
}

使用者端 client.cpp:

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")  //載入 ws2_32.dll

#define BUF_SIZE 100

int main(){
    //初始化DLL
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);

    //建立通訊端
    SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

    //向伺服器發起請求
    sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr));  //每個位元組都用0填充
    sockAddr.sin_family = PF_INET;
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    sockAddr.sin_port = htons(1234);
    connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
    //獲取使用者輸入的字串並行送給伺服器
    char bufSend[BUF_SIZE] = {0};
    printf("Input a string: ");
    scanf("%s", bufSend);
    send(sock, bufSend, strlen(bufSend), 0);
    //接收伺服器傳回的資料
    char bufRecv[BUF_SIZE] = {0};
    recv(sock, bufRecv, BUF_SIZE, 0);

    //輸出接收到的資料
    printf("Message form server: %sn", bufRecv);

    //關閉通訊端
    closesocket(sock);

    //終止使用 DLL
    WSACleanup();

    system("pause");
    return 0;
}

先執行伺服器端,再執行使用者端,執行結果為: Input a string: c-language java cpp↙ Message form server: c-language

scanf() 讀取到空格時認為一個字串輸入結束,所以只能讀取到“c-language”;如果不希望把空格作為字串的結束符,可以使用 gets() 函數。

通過本程式可以發現,使用者端也可以向伺服器端傳送資料,這樣伺服器端就可以根據不同的請求作出不同的響應,http 伺服器就是典型的例子,請求的網址不同,返回的頁面也不同。

如何讓伺服器端持續監聽客戶端的請求?

前面的程序,不管伺服器端還是客戶端,都有一個問題,就是處理完一個請求立即退出了,沒有太大的實際意義。能不能像Web伺服器那樣一直接受客戶端的請求呢?能,使用 while 循環即可。

修改前面的回聲程序,使伺服器端可以不斷響應客戶端的請求。

伺服器端 server.cpp:

#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib") //載入 ws2_32.dll

#define BUF_SIZE 100

int main()
{
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);

    //建立套接字
    SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);

    //繫結套接字
    sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr)); //每個位元組都用0填充
    sockAddr.sin_family = PF_INET; //使用IPv4地址
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址
    sockAddr.sin_port = htons(1234); //連接埠
    bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

    //進入監聽狀態
    listen(servSock, 20);

    //接收客戶端請求
    SOCKADDR clntAddr;
    int nSize = sizeof(SOCKADDR);
    char buffer[BUF_SIZE] = {0}; //緩衝區

    while (1) {
        SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
        int strLen = recv(clntSock, buffer, BUF_SIZE,
                          0); //接收客戶端發來的資料
        send(clntSock, buffer, strLen, 0); //將資料原樣返回

        closesocket(clntSock); //關閉套接字
        memset(buffer, 0, BUF_SIZE); //重設緩衝區
    }

    //關閉套接字
    closesocket(servSock);

    //終止 DLL 的使用
    WSACleanup();
    return 0;
}

客戶端 client.cpp:

#include <stdio.h>
#include <WinSock2.h>
#include <windows.h>
#pragma comment(lib, "ws2_32.lib") //載入 ws2_32.dll

#define BUF_SIZE 100

int main()
{
    //初始化DLL
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);

    //向伺服器發起請求
    sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr)); //每個位元組都用0填充
    sockAddr.sin_family = PF_INET;
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    sockAddr.sin_port = htons(1234);

    char bufSend[BUF_SIZE] = {0};
    char bufRecv[BUF_SIZE] = {0};

    while (1) {
        //建立套接字
        SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
        connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
        //獲取使用者輸入的字串並行送給伺服器
        printf("Input a string: ");
        gets(bufSend);
        send(sock, bufSend, strlen(bufSend), 0);
        //接收伺服器傳回的資料
        recv(sock, bufRecv, BUF_SIZE, 0);
        //輸出接收到的資料
        printf("Message form server: %s\n", bufRecv);

        memset(bufSend, 0, BUF_SIZE); //重設緩衝區
        memset(bufRecv, 0, BUF_SIZE); //重設緩衝區
        closesocket(sock); //關閉套接字
    }

    WSACleanup(); //終止使用 DLL
    return 0;
}

先運行伺服器端,再運行客戶端,結果如下: Input a string: c language Message form server: c language Input a string: C語言中文網 Message form server: C語言中文網 Input a string: 學習C/C++程式設計的好網站 Message form server: 學習C/C++程式設計的好網站

while(1) 讓程式碼進入死循環,除非使用者關閉程序,否則伺服器端會一直監聽客戶端的請求。客戶端也是一樣,會不斷向伺服器發起連接。

需要注意的是:server.cpp 中呼叫 closesocket() 不僅會關閉伺服器端的 socket,還會通知客戶端連接已斷開,客戶端也會清理 socket 相關資源,所以 client.cpp 中需要將 socket() 放在 while 循環內部,因為每次請求完畢都會清理 socket,下次發起請求時需要重新建立。後續我們會進行詳細講解。

socket緩衝區以及阻塞模式詳解

在《socket資料的接收和傳送》一節中講到,可以使用 write()/send() 函數傳送資料,使用 read()/recv() 函數接收資料,本節就來看看資料是如何傳遞的。

socket緩衝區

每個 socket 被建立後,都會分配兩個緩衝區,輸入緩衝區和輸出緩衝區。

write()/send() 並不立即向網路中傳輸資料,而是先將資料寫入緩衝區中,再由TCP協議將資料從緩衝區傳送到目標機器。一旦將資料寫入到緩衝區,函數就可以成功返回,不管它們有沒有到達目標機器,也不管它們何時被傳送到網路,這些都是TCP協議負責的事情。

TCP協議獨立於 write()/send() 函數,資料有可能剛被寫入緩衝區就傳送到網路,也可能在緩衝區中不斷積壓,多次寫入的資料被一次性傳送到網路,這取決於當時的網路情況、當前執行緒是否空閒等諸多因素,不由程式設計師控制。

read()/recv() 函數也是如此,也從輸入緩衝區中讀取資料,而不是直接從網路中讀取。

TCP套接字的I/O緩衝區示意圖 圖:TCP套接字的I/O緩衝區示意圖

這些I/O緩衝區特性可整理如下:

  • I/O緩衝區在每個TCP套接字中單獨存在;
  • I/O緩衝區在建立套接字時自動生成;
  • 即使關閉套接字也會繼續傳送輸出緩衝區中遺留的資料;
  • 關閉套接字將丟失輸入緩衝區中的資料。

輸入輸出緩衝區的默認大小一般都是 8K,可以通過 getsockopt() 函數獲取:

unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\n", optVal);

運行結果: Buffer length: 8192

這裡僅給出示例,後面會詳細講解。

阻塞模式

對於TCP套接字(默認情況下),當使用 write()/send() 傳送資料時:

  • 首先會檢查緩衝區,如果緩衝區的可用空間長度小於要傳送的資料,那麼 write()/send() 會被阻塞(暫停執行),直到緩衝區中的資料被傳送到目標機器,騰出足夠的空間,才喚醒 write()/send() 函數繼續寫入資料。

  • 如果TCP協議正在向網路傳送資料,那麼輸出緩衝區會被鎖定,不允許寫入,write()/send() 也會被阻塞,直到資料傳送完畢緩衝區解鎖,write()/send() 才會被喚醒。

  • 如果要寫入的資料大於緩衝區的最大長度,那麼將分批寫入。

  • 直到所有資料被寫入緩衝區 write()/send() 才能返回。

當使用 read()/recv() 讀取資料時:

  • 首先會檢查緩衝區,如果緩衝區中有資料,那麼就讀取,否則函數會被阻塞,直到網路上有資料到來。

  • 如果要讀取的資料長度小於緩衝區中的資料長度,那麼就不能一次性將緩衝區中的所有資料讀出,剩餘資料將不斷積壓,直到有 read()/recv() 函數再次讀取。

  • 直到讀取到資料後 read()/recv() 函數才會返回,否則就一直被阻塞。

這就是TCP套接字的阻塞模式。所謂阻塞,就是上一步動作沒有完成,下一步動作將暫停,直到上一步動作完成後才能繼續,以保持同步性。

TCP套接字默認情況下是阻塞模式,也是最常用的。當然你也可以更改為非阻塞模式,後續我們會講解。

TCP協議的粘包問題(資料的無邊界性)

上節我們講到了socket緩衝區和資料的傳遞過程,可以看到資料的接收和傳送是無關的,read()/recv() 函數不管資料傳送了多少次,都會儘可能多的接收資料。也就是說,read()/recv() 和 write()/send() 的執行次數可能不同。

例如,write()/send() 重複執行三次,每次都傳送字串"abc",那麼目標機器上的 read()/recv() 可能分三次接收,每次都接收"abc";也可能分兩次接收,第一次接收"abcab",第二次接收"cabc";也可能一次就接收到字串"abcabcabc"。

假設我們希望客戶端每次傳送一位學生的學號,讓伺服器端返回該學生的姓名、住址、成績等資訊,這時候可能就會出現問題,伺服器端不能區分學生的學號。例如第一次傳送 1,第二次傳送 3,伺服器可能當成 13 來處理,返回的資訊顯然是錯誤的。

這就是資料的“粘包”問題,客戶端傳送的多個封包被當做一個封包接收。也稱資料的無邊界性,read()/recv() 函數不知道封包的開始或結束標誌(實際上也沒有任何開始或結束標誌),只把它們當做連續的資料流來處理。

下面的程式碼演示了粘包問題,客戶端連續三次向伺服器端傳送資料,伺服器端卻一次性接收到所有資料。

伺服器端程式碼 server.cpp:

#include <stdio.h>
#include <windows.h>
#pragma comment (lib, "ws2_32.lib")  //載入 ws2_32.dll
#define BUF_SIZE 100
int main()
{
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);
    //建立套接字
    SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);
    //繫結套接字
    struct sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr));  //每個位元組都用0填充
    sockAddr.sin_family = PF_INET;  //使用IPv4地址
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具體的IP地址
    sockAddr.sin_port = htons(1234);  //連接埠
    bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
    //進入監聽狀態
    listen(servSock, 20);
    //接收客戶端請求
    SOCKADDR clntAddr;
    int nSize = sizeof(SOCKADDR);
    char buffer[BUF_SIZE] = {0};  //緩衝區
    SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
    Sleep(10000);  //注意這裡,讓程序暫停10秒
    //接收客戶端發來的資料,並原樣返回
    int recvLen = recv(clntSock, buffer, BUF_SIZE, 0);
    send(clntSock, buffer, recvLen, 0);
    //關閉套接字並終止DLL的使用
    closesocket(clntSock);
    closesocket(servSock);
    WSACleanup();
    return 0;
}

客戶端程式碼 client.cpp:

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")  //載入 ws2_32.dll

#define BUF_SIZE 100

int main()
{
    //初始化DLL
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);

    //向伺服器發起請求
    struct sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr));  //每個位元組都用0填充
    sockAddr.sin_family = PF_INET;
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    sockAddr.sin_port = htons(1234);

    //建立套接字
    SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

    //獲取使用者輸入的字串並行送給伺服器
    char bufSend[BUF_SIZE] = {0};
    printf("Input a string: ");
    gets(bufSend);

    for (int i = 0; i < 3; i++) {
        send(sock, bufSend, strlen(bufSend), 0);
    }

    //接收伺服器傳回的資料
    char bufRecv[BUF_SIZE] = {0};
    recv(sock, bufRecv, BUF_SIZE, 0);
    //輸出接收到的資料
    printf("Message form server: %s\n", bufRecv);

    closesocket(sock);  //關閉套接字
    WSACleanup();  //終止使用 DLL

    system("pause");
    return 0;
}

先運行 server,再運行 client,並在10秒內輸入字串"abc",再等數秒,伺服器就會返回資料。運行結果如下: Input a string: abc Message form server: abcabcabc

本程序的關鍵是 server.cpp 第31行的程式碼Sleep(10000);,它讓程序暫停執行10秒。在這段時間內,client 連續三次傳送字串"abc",由於 server 被阻塞,資料只能堆積在緩衝區中,10秒後,server 開始運行,從緩衝區中一次性讀出所有積壓的資料,並返回給客戶端。

另外還需要說明的是 client.cpp 第34行程式碼。client 執行到 recv() 函數,由於輸入緩衝區中沒有資料,所以會被阻塞,直到10秒後 server 傳回資料才開始執行。使用者看到的直觀效果就是,client 暫停一段時間才輸出 server 返回的結果。

client 的 send() 傳送了三個封包,而 server 的 recv() 卻只接收到一個封包,這很好的說明瞭資料的粘包問題。

圖解TCP資料報結構以及三次握手(非常詳細)

TCP(Transmission Control Protocol,傳輸控制協議)是一種面向連接的、可靠的、基於位元組流的通訊協議,資料在傳輸前要建立連接,傳輸完畢後還要斷開連接。

客戶端在收發資料前要使用 connect() 函數和伺服器建立連接。建立連接的目的是保證IP地址、連接埠、物理鏈路等正確無誤,為資料的傳輸開闢通道。

TCP建立連接時要傳輸三個封包,俗稱三次握手(Three-way Handshaking)。可以形象的比喻為下面的對話:

  • [Shake 1] 套接字A:“你好,套接字B,我這裡有資料要傳送給你,建立連接吧。”
  • [Shake 2] 套接字B:“好的,我這邊已準備就緒。”
  • [Shake 3] 套接字A:“謝謝你受理我的請求。”

TCP資料報結構

我們先來看一下TCP資料報的結構: img

帶陰影的幾個欄位需要重點說明一下:

  • 序號:Seq(Sequence Number)序號佔32位,用來標識從電腦A傳送到電腦B的封包的序號,電腦傳送資料時對此進行標記。

  • 確認號:Ack(Acknowledge Number)確認號佔32位,客戶端和伺服器端都可以傳送,Ack = Seq + 1。

  • 標誌位:每個標誌位佔用1Bit,共有6個,分別為 URG、ACK、PSH、RST、SYN、FIN,具體含義如下:

    • URG:緊急指針(urgent pointer)有效。
    • ACK:確認序號有效。
    • PSH:接收方應該盡快將這個報文交給應用層。
    • RST:重設連接。
    • SYN:建立一個新連接。
    • FIN:斷開一個連接。

對英文字母縮寫的總結:Seq 是 Sequence 的縮寫,表示序列;Ack(ACK) 是 Acknowledge 的縮寫,表示確認;SYN 是 Synchronous 的縮寫,願意是“同步的”,這裡表示建立同步連接;FIN 是 Finish 的縮寫,表示完成。

連接的建立(三次握手)

使用 connect() 建立連接時,客戶端和伺服器端會相互傳送三個封包,請看下圖: img

客戶端呼叫 socket() 函數建立套接字後,因為沒有建立連接,所以套接字處於CLOSED狀態;伺服器端呼叫 listen() 函數後,套接字進入LISTEN狀態,開始監聽客戶端請求。

這個時候,客戶端開始發起請求:

  • 當客戶端呼叫 connect() 函數後,TCP協議會組建一個封包,並設定 SYN 標誌位,表示該封包是用來建立同步連接的。同時生成一個隨機數字 1000,填充“序號(Seq)”欄位,表示該封包的序號。完成這些工作,開始向伺服器端傳送封包,客戶端就進入了SYN-SEND狀態。

  • 伺服器端收到封包,檢測到已經設定了 SYN 標誌位,就知道這是客戶端發來的建立連接的“請求包”。伺服器端也會組建一個封包,並設定 SYN 和 ACK 標誌位,SYN 表示該封包用來建立連接,ACK 用來確認收到了剛才客戶端傳送的封包。伺服器生成一個隨機數 2000,填充“序號(Seq)”欄位。2000 和客戶端封包沒有關係。伺服器將客戶端封包序號(1000)加1,得到1001,並用這個數字填充“確認號(Ack)”欄位。伺服器將封包發出,進入SYN-RECV狀態。

  • 客戶端收到封包,檢測到已經設定了 SYN 和 ACK 標誌位,就知道這是伺服器發來的“確認包”。客戶端會檢測“確認號(Ack)”欄位,看它的值是否為 1000+1,如果是就說明連接建立成功。接下來,客戶端會繼續組建封包,並設定 ACK 標誌位,表示客戶端正確接收了伺服器發來的“確認包”。同時,將剛才伺服器發來的封包序號(2000)加1,得到 2001,並用這個數字來填充“確認號(Ack)”欄位。客戶端將封包發出,進入ESTABLISED狀態,表示連接已經成功建立。

  • 伺服器端收到封包,檢測到已經設定了 ACK 標誌位,就知道這是客戶端發來的“確認包”。伺服器會檢測“確認號(Ack)”欄位,看它的值是否為 2000+1,如果是就說明連接建立成功,伺服器進入ESTABLISED狀態。

至此,客戶端和伺服器都進入了ESTABLISED狀態,連接建立成功,接下來就可以收發資料了。

最後的說明

三次握手的關鍵是要確認對方收到了自己的封包,這個目標就是通過“確認號(Ack)”欄位實現的。電腦會記錄下自己傳送的封包序號 Seq,待收到對方的封包後,檢測“確認號(Ack)”欄位,看Ack = Seq + 1是否成立,如果成立說明對方正確收到了自己的封包。

詳細分析TCP資料的傳輸過程

建立連接後,兩臺主機就可以相互傳輸資料了。如下圖所示:

TCP 套接字的資料交換過程 圖1:TCP 套接字的資料交換過程

上圖給出了主機A分2次(分2個封包)向主機B傳遞200位元組的過程。首先,主機A通過1個封包傳送100個位元組的資料,封包的 Seq 號設定為 1200。主機B為了確認這一點,向主機A傳送 ACK 包,並將 Ack 號設定為 1301。

為了保證資料精準到達,目標機器在收到封包(包括SYN包、FIN包、普通封包等)包後必須立即回傳ACK包,這樣傳送方才能確認資料傳輸成功。

此時 Ack 號為 1301 而不是 1201,原因在於 Ack 號的增量為傳輸的資料位元組數。假設每次 Ack 號不加傳輸的位元組數,這樣雖然可以確認封包的傳輸,但無法明確100位元組全部正確傳遞還是丟失了一部分,比如只傳遞了80位元組。因此按如下的公式確認 Ack 號:

Ack號 = Seq號 + 傳遞的位元組數 + 1

與三次握手協議相同,最後加 1 是為了告訴對方要傳遞的 Seq 號。

下面分析傳輸過程中封包丟失的情況,如下圖所示:

img 圖2:TCP套接字資料傳輸過程中發生錯誤

上圖表示通過 Seq 1301 封包向主機B傳遞100位元組的資料,但中間發生了錯誤,主機B未收到。經過一段時間後,主機A仍未收到對於 Seq 1301 的ACK確認,因此嘗試重傳資料。

為了完成封包的重傳,TCP套接字每次傳送封包時都會啟動定時器,如果在一定時間內沒有收到目標機器傳回的 ACK 包,那麼定時器超時,封包會重傳。

上圖演示的是封包丟失的情況,也會有 ACK 包丟失的情況,一樣會重傳。

重傳超時時間(RTO, Retransmission Time Out)

這個值太大了會導致不必要的等待,太小會導致不必要的重傳,理論上最好是網路 RTT 時間,但又受制於網路距離與瞬態時延變化,所以實際上使用自適應的動態演算法(例如 Jacobson 演算法和 Karn 演算法等)來確定超時時間。

往返時間(RTT,Round-Trip Time)表示從傳送端傳送資料開始,到傳送端收到來自接收端的 ACK 確認包(接收端收到資料後便立即確認),總共經歷的時延。

重傳次數

TCP封包重傳次數根據系統設定的不同而有所區別。有些系統,一個封包只會被重傳3次,如果重傳3次後還未收到該封包的 ACK 確認,就不再嘗試重傳。但有些要求很高的業務系統,會不斷地重傳丟失的封包,以盡最大可能保證業務資料的正常互動。

最後需要說明的是,傳送端只有在收到對方的 ACK 確認包後,才會清空輸出緩衝區中的資料。

如何在Linux上通過cgroup限制一個處理程序使用CPU和記憶體

https://blog.csdn.net/weixin_37871174/article/details/130390336

Cgroup(Control Group)是 Linux 核心的一個功能,可以通過它來限制處理程序的 CPU 和記憶體佔用。Cgroup 實現了對系統資源的細粒度控制和管理,可以將一組處理程序放入同一個 Cgroup 中,並對該 Control Group 中的所有處理程序共享相應的資源配額。

下面舉個實際的例子,演示如何使用 Cgroup 限制一個處理程序的 CPU 佔用率和記憶體使用量:

  1. 首先需要安裝 cgroup 工具包,在 Ubuntu 系統上可以執行以下命令進行安裝:

    sudo apt-get install cgroup-tools
    
  2. 建立一個名為 mycg 的控制組,以限制該組中的處理程序的 CPU 佔用率和記憶體使用量。在 shell 終端輸入下列命令:

    sudo mkdir /sys/fs/cgroup/cpu_mytainer
    sudo mkdir /sys/fs/cgroup/memory_mytainer
    
  3. 設定 cpu 資源限制:

    echo "10000" > /sys/fs/cgroup/cpu_mytainer/cpu.cfs_quota_us #設定每10ms分配給cgroup桶的最大時間片值 
    echo "200000" > /sys/fs/cgroup/cpu_mytainer/cpu.cfs_period_us #設定每次時間輪轉過多少微秒 
    

這兩行程式碼告訴核心同時運行的程序切換超時參數,即當前可佔用 10ms 核心時間,然後必須讓出時間,並等待 200ms 核心時間過後再佔用,以達到限制CPU使用的目地。

  1. 設定memory資源限制:

    echo "50M" > /sys/fs/cgroup/memory_mytainer/memory.limit_in_bytes #設定cgroup總共最多能夠使用記憶體大小
    

    這條命令表示限制 mycg 這個 Cgroup 的處理程序總佔用記憶體不得超過 50MB。

  2. 建立一個新處理程序並將它加入 mycg 中,然後觀察該處理程序利用率是否受到限制。例如我們建立一個死循環 c

    #include <stdio.h>
    
    int main()
    {
        while(1){
            int a=100000000,b;
            b=a/b;
        }
        return 0;
    }
    

編譯成可執行檔案 test.out 並運行如下程式碼:

sudo cgcreate -a root:root -g cpu_mytainer,memory_mytainer:/mycg  
sudo echo $PID >>/sys/fs/cgroup/cpu_mytainer/tasks 
sudo echo $PID >>/sys/fs/cgroup/memory_mytainer/tasks  

其中 PID 是指上面循環程序 test.out 的處理程序 ID。

  1. 使用 ps 命令檢查處理程序的CPU和記憶體使用情況:

    ps aux | grep test.out
    

    你可以看到產生了類似以下的輸出:

USER      PID    %CPU     %MEM            VSZ         RSS       TTY     STAT      START         TIME        COMMAND
root     3833    10.0     0.1           62820        2580   pts/9    R     11:56         00:00:30     ./test.out

說明測試程序的CPU使用率已經被成功限制在10%以內,而記憶體佔用不會超過50MB。

limit chrome

sudo chmod o+w /sys/fs/cgroup/cgroup.procs


sudo cgcreate -t $USER:$USER -a $USER:$USER  -g memory,cpuset:limitchrome
# Limit RAM to 1.5G roughly
echo 1600000000 | sudo tee /sys/fs/cgroup/limitchrome/memory.limit_in_bytes

echo 0-4 | sudo tee /sys/fs/cgroup/limitchrome/cpuset.cpus
# Run chrome in this cgroup
cgexec -g memory,cpuset:limitchrome /opt/google/chrome/google-chrome --profile-directory=Default
# Delete the cgroup (not required)
sudo cgdelete -g memory,cpuset:limitchrome

Linux 效能分析工具: Perf

簡介

Perf 全名是 Performance Event,是在 Linux 2.6.31 以後內建的系統效能分析工具,它隨著核心一併釋出。藉由 perf,應用程式可以利用 PMU (Performance Monitoring Unit), tracepoint 和核心內部的特殊計數器 (counter) 來進行統計,另外還能同時分析運行中的核心程式碼,從而更全面瞭解應用程式中的效能瓶頸。

相較於 OProfileGProf ,perf 的優勢在於與 Linux Kernel 緊密結合,並可受益於最先納入核心的新特徵。perf 基本原理是對目標進行取樣,紀錄特定的條件下所偵測的事件是否發生以及發生的次數。例如根據 tick 中斷進行取樣,即在 tick 中斷內觸發取樣點,在取樣點裡判斷行程 (process) 當時的 context。假如一個行程 90% 的時間都花費在函式 foo() 上,那麼 90% 的取樣點都應該落在函式 foo() 的上下文中。

Perf 可取樣的事件非常多,可以分析 Hardware event,如 cpu-cycles、instructions 、cache-misses、branch-misses …等等。可以分析 Software event,如 page-faults、context-switches …等等,另外一種就是 Tracepoint event。知道了 cpu-cycles、instructions 我們可以瞭解 Instruction per cycle 是多少,進而判斷程式碼有沒有好好利用 CPU,cache-misses 可以曉得是否有善用 Locality of reference ,branch-misses 多了是否導致嚴重的 pipeline hazard?另外 Perf 還可以對函式進行採樣,瞭解效能卡在哪邊。

安裝

首先利用以下指令查看目前的 Kernel config 有沒有啟用 Perf。如果 PC 上是裝一般 Linux distro,預設值應該都有開啟。

$ cat "/boot/config-`uname -r`" | grep "PERF_EVENT"

如果自己編譯核心可以參照這篇文章來啟用 perf。

參考的環境是 Ubuntu 14.04,kernel 版本 3.16.0。有兩種方法可以安裝

  1. 前面講到,perf 是 Linux 內建支持的效能優化工具,在 2.6.31 版本之後,我們可以直接到 Linux Kernel Archives 下載對應版本的程式碼,解壓縮後到 tools/perf 裡面去編譯,通常過程中會有相依的套件需要安裝,依指示完成安裝後,編譯即可成功,最後再把編譯完成的 perf 移至 /usr/bin 中就可以使用了。 這種方法通常適用於更新過 kernel 的使用者,因為更新過 kernel 後會造成 distribution package 與 kernel version 不相符。一般使用者採用第二種方法即可。
  2. 使用 apt-get 進行安裝。
$ sudo apt-get install linux-tools-common

接著輸入 perf list 或 perf top 檢查一下 perf 可不可以使用。

如果出現以下的訊息,表示還漏了些東西。

WARNING: perf not found for kernel 3.16.0-50
You may need to install the following packages for this specific kernel:
    linux-tools-3.16.0-50-generic
    linux-cloud-tools-3.16.0-50-generic

上面的 Kernel 版本可能和你不一樣,根據指示安裝起來即可。不放心的話可以使用$ uname -r確認。

$ sudo apt-get install linux-tools-3.16.0-50-generic linux-cloud-tools-3.16.0-50-generic
  1. 到這裡 perf 的安裝就完成了。不過這裡我再稍微補充一下,如果你不是切換到 root 的情況下輸入
$ perf top

其實會出現以下錯誤畫面。

img

kernel.perf_event_paranoid 是用來決定你在沒有 root 權限下 (Normal User) 使用 perf 時,你可以取得哪些 event data。預設值是 1 ,你可以輸入

$ cat /proc/sys/kernel/perf_event_paranoid

來查看權限值。一共有四種權限值:

2 : 不允許任何量測。但部份用來查看或分析已存在的紀錄的指令仍可使用,如 perf ls、perf report、perf timechart、 perf trace。

1 : 不允許 CPU events data。但可以使用 perf stat、perf record 並取得 Kernel profiling data。

0 : 不允許 raw tracepoint access。但可以使用 perf stat、perf record 並取得 CPU events data。

-1: 權限全開。

最後如果要檢測 cache miss event ,需要先取消 kernel pointer 的禁用。

$ sudo sh -c " echo 0 > /proc/sys/kernel/kptr_restrict"

先來個範例暖身吧!

一開始,我們先使用第一次作業 「計算圓周率」 的程式來體會一下 perf 使用。 [perf_top_example.c]

#include <stdio.h>
#include <unistd.h>

double compute_pi_baseline(size_t N) {
    double pi = 0.0;
    double dt = 1.0 / N;
    for (size_t i = 0; i < N; i++) {
        double x = (double) i / N;
        pi += dt / (1.0 + x * x);
    }
    return pi * 4.0;
}
int main() {
    printf("pid: %d\n", getpid());
    sleep(10);
    compute_pi_baseline(50000000);
    return 0;
}

將上述程式存檔為 perf_top_example.c,並執行:

g++ -c perf_top_example.c
g++ perf_top_example.o -o example
./example

執行上述程式後,可以取得一個 pid 值,再根據 pid 輸入

perf top -p $pid

應該會得到類似下面的結果:

img

預設的 performance event 是 「cycles」,所以這條指令可以分析出消耗 CPU 週期最多的部份,結果顯示函式 compute_pi_baseline() 佔了近 99.9%,跟預期一樣,此函式是程式中的「熱點」!有了一些感覺後,後面會詳細一點介紹 perf 用法。

背景知識

以下節錄上海交大通信與電子工程系的劉明寫的文章:

Perf – Linux下的系統性能調優工具

簡繁體中文詞彙對照:科技纇 (本課程斟酌修改詞彙,==> 開頭表示補充)

  • 背景知識

有些背景知識是分析性能問題時需要瞭解的。比如硬件 cache;再比如作業系統核心。應用程式的行為細節往往是和這些東西互相牽扯的,這些底層的東西會以意想不到的方式影響應用程式的性能,比如某些程式無法充分利用 cache,從而導致性能下降。比如不必要地呼叫過多的系統呼叫,造成頻繁的核心 / 使用者層級的切換 …等等。這裡只是為本文的後續內容做些概述,關於效能調校還有很多東西。

  • 效能相關的處理器硬體特性,PMU 簡介

當演算法已趨於最佳化,程式碼不斷精簡,人們調到最後,便需要斤斤計較了。cache、pipeline 等平時不大注意的東西也必須精打細算了。

  • 硬體特性之 cache

記憶體存取很快,但仍無法和處理器的指令執行速度相提並論。為了從記憶體中讀取指令 (instruction) 和資料 (data),處理器需要等待,用處理器的時間來衡量,這種等待非常漫長。cache 是一種 SRAM,它的存取速率非常快,與處理器處理速度較為接近。因此將常用的資料保存在 cache 中,處理器便無須等待,從而提高效能。cache 的尺寸一般都很小,充分利用 cache 是軟體效能改善過程中,非常重要的部分。

  • 硬體特性之 pipeline, superscalar, out-ot-order execution

提昇效能最有效的方式之一就是平行 (parallelism)。處理器在設計時也儘可能地平行,比如 pipeline, superscalar, out-of-execution。

處理器處理一條指令需要分多個步驟完成,比如 fetch 指令,然後完成運算,最後將計算結果輸出到匯流排 (bus) 上。在處理器內部,這可以看作一個三級 pipeline,如下圖處理器 pipeline 所示:

img

指令從左邊進入處理器,上圖中的 pipeline 有三級,一個時鐘週期內可以同時處理三條指令,分別被 pipeline 的不同部分處理。

Superscalar 指一個時鐘週期觸發 (issue) 多條指令的 pipeline機器架構,比如 Intel 的 Pentium 處理器,內部有兩個執行單元,在一個時鐘週期內允許執行兩條指令。

==> 這樣稱為 dual-issue,可想像為一個 packet 裡同時有兩組 pipelined 的 instruction

==> 比方說,Cortex-A5 和 Cortex-A8 一樣採用 ARMv7-A 指令集,但是 Cortex-A5 是 Cortext-A8/A9 的精簡版,有以下差異:

1.pipeline 自 13 stages 減為 8 stages 2.instruction 自 dual-issue 減為 single-issue 3.NEON/FPU 為選配 4.不具有 L2 Cache

此外,在處理器內部,不同指令所需要的執行時間和時鐘週期是不同的,如果嚴格按照程序的執行順序執行,那麼就無法充分利用處理器的 pipeline。因此指令有可能被亂序執行 (out-of-order execution)。

上述三種平行技術對所執行的指令有一個基本要求,即相鄰的指令相互沒有依賴關係。假如某條指令需要依賴前面一條指令的執行結果數據,那麼 pipeline 便失去作用,因為第二條指令必須等待第一條指令完成。因此好的軟體必須儘量避免產生這種程式碼。

  • 硬體特性之 branch prediction

branch prediction 指令對軟體效能影響較大。尤其是當處理器採用流水線設計之後,假設 pipeline 有三級,且目前進入 pipeline 的第一道指令為分支 (branch) 指令。假設處理器順序讀取指令,那麼如果分支的結果是跳躍到其他指令,那麼被處理器 pipeline 所 fetch 的後續兩條指令勢必被棄置 (來不及執行),從而影響性能。為此,很多處理器都提供了 branch prediction,根據同一條指令的歷史執行記錄進行預測,讀取最可能的下一條指令,而並非順序讀取指令。

==> 搭配簡報: Branch Prediction

branch prediction 對軟體架構有些要求,對於重複性的分支指令序列,branch prediction 硬體才能得到較好的預測結果,而對於類似 switch-case 一類的程式結構,則往往不易得到理想的預測結果。

==> 對照閱讀: Fast and slow if-statements: branch prediction in modern processors

==> 編譯器提供的輔助機制: Branch Patterns, Using GCC

上面介紹的幾種處理器特性對軟體效能影響很大,然而依賴時鐘進行定期採樣的 profiler 模式無法闡述程式對這些處理器硬體特性的使用情況。處理器廠商針對這種情況,在硬體中加入了 PMU (performance monitor unit)。PMU 允許硬體針對某種事件設置 counter,此後處理器便開始統計該事件的發生次數,當發生的次數超過 counter 內設定的數值後,便產生中斷。比如 cache miss 達到某個值後,PMU 便能產生相應的中斷。一旦捕獲這些中斷,便可分析程式對這些硬體特性的使用率了。

  • Tracepoints

Tracepoint 是散落在核心原始程式碼的一些 hook,一旦使能,在指定的程式碼被運行時,tracepoint 就會被觸發,這樣的特性可被各種 trace/debug 工具所使用,perf 就是這樣的案例。若你想知道在應用程式執行時期,核心記憶體管理模組的行為,即可透過潛伏在 slab 分配器中的 tracepoint。當核心運行到這些 tracepoint 時,便會通知 perf。

Perf 將 tracepoint 產生的事件記錄下來,生成報告,通過分析這些報告,效能分析調校的工程人員便可瞭解程式執行時期的核心種種細節,也能做出針對效能更準確的診斷。

Perf 基本使用

前面有提到,Perf 能觸發的事件分為三類:

  • hardware : 由 PMU 產生的事件,比如 cache-misses、cpu-cycles、instructions、branch-misses …等等,通常是當需要瞭解程序對硬體特性的使用情況時會使用。
  • software : 是核心程式產生的事件,比如 context-switches、page-faults、cpu-clock、cpu-migrations …等等。
  • tracepoint : 是核心中的靜態 tracepoint 所觸發的事件,這些 tracepoint 用來判斷在程式執行時期,核心的行為細節,比如 slab 記憶體配置器的配置次數等。

Perf 包含 20 幾種子工具集,不過我還沒碰過很多,我根據目前理解先介紹以下。 如果想看第一手資料

$ perf help <command>

###perf list 這應該是大部分的人第一次安裝 perf 後所下的第一個指令,它能印出 perf 可以觸發哪些 event,不同 CPU 可能支援不同 hardware event,不同 kernel 版本支援的 software、tracepoint event 也不同。我的 perf 版本是3.19.8,所支援的 event 已經超過 1400 項(另外要列出 Tracepoint event 必須開啟 root 權限)。

$ perf list

img

perf top

perf top 其實跟平常 Linux 內建的 top 指令很相似。它能夠「即時」的分析各個函式在某個 event 上的熱點,找出拖慢系統的凶手,就如同上面那個範例一樣。甚至,即使沒有特定的程序要觀察,你也可以直接下達 $ perf top 指令來觀察是什麼程序吃掉系統效能,導致系統異常變慢。譬如我執行一個無窮迴圈:

int main() {
    long int i = 0;
    while(1) {
        i++;
        add(i);
        div(i);
    }
    return 0;
}

可以發現紅色熱點就出現了。右邊第一列為各函式的符號,左邊第一行是該符號引發的 event 在整個「監視域」中佔的比例,我們稱作該符號的熱度,監視域指的是 perf 監控的所有符號,預設值包括系統所有程序、核心以及核心 module 的函式,左邊第二行則為該符號所在的 Shared Object 。若符號旁顯示[.]表示其位於 User mode,[k]則為 kernel mode。

img

(當你關掉該程序之後,這個監視畫面 (tui 界面) 裡的該程序不會「馬上」消失,而是其 overhead 的比例一直減少然後慢慢離開列表)。

按下 h可以呼叫 help ,它會列出 perf top 的所有功能和對應按鍵。 我們來試看看 Annotate(註解),這功能可以進一步深入分析某個符號。使用方向鍵移到你有興趣的符號按下a。 它會顯示各條指令的 event 取樣率(耗時較多的部份就容易被 perf 取樣到)。

img

最後若你想要觀察其他 event ( 預設 cycles ) 和指定取樣頻率 ( 預設每秒4000次 ) :

$ perf top -e cache-misses -c 5000

perf stat

相較於 top,使用 perf stat 往往是你已經有個要優化的目標,對這個目標進行特定或一系列的 event 檢查,進而瞭解該程序的效能概況。(event 沒有指定的話,預設會有十種常用 event。) 我們來對以下程式使用 perf stat 工具 分析 cache miss 情形

static char array[10000][10000];
int main (void){
  int i, j;
  for (i = 0; i < 10000; i++)
    for (j = 0; j < 10000; j++)
       array[j][i]++;
  return 0;
}
$ perf stat --repeat 5 -e cache-misses,cache-references,instructions,cycles ./perf_stat_cache_miss
    Performance counter stats for './perf_stat_cache_miss' (5 runs):
    4,416,226        cache-misses        #    3.437 % of all cache refs    ( +-  0.27% )
    128,483,262      cache-references                                      ( +-  0.02% )
    2,123,281,496    instructions        #    0.65  insns per cycle        ( +-  0.02% )
    3,281,498,034    cycles                                                ( +-  0.21% )
        1.299352302 seconds time elapsed                                   ( +-  0.19% )

--repeat <n>或是-r <n> 可以重複執行 n 次該程序,並顯示每個 event 的變化區間。 cache-misses,cache-referencesinstructions,cycles類似這種成對的 event,若同時出現 perf 會很貼心幫你計算比例。

根據這次 perf stat 結果可以明顯發現程序有很高的 cache miss,連帶影響 IPC 只有0.65

如果我們善用一下存取的局部性,將 i,j對調改成array[i][j]++

    Performance counter stats for './perf_stat_cache_miss' (5 runs):
    2,263,131        cache-misses        #   93.742 % of all cache refs    ( +-  0.53% )
    2,414,202        cache-references                                      ( +-  1.82% )
    2,123,275,176    instructions        #    1.98  insns per cycle        ( +-  0.03% )
    1,074,868,730    cycles                                                ( +-  1.96% )
        0.432727146 seconds time elapsed                                   ( +-  1.99% )

cache-references 從 128,483,262下降到 2,414,202,差了五十幾倍,執行時間也縮短為原來的三分之一!

###perf record & perf report 有別於 stat,record 可以針對函式級別進行 event 統計,方便我們對程序「熱點」作更精細的分析和優化。 我們來對以下程式,使用 perf record 進行 branch 情況分析

#define N 5000000
static int array[N] = { 0 };
void normal_loop(int a) {
    int i;
    for (i = 0; i < N; i++)
        array[i] = array[i]+a;
}
void unroll_loop(int a) {
    int i;
    for (i = 0; i < N; i+=5){
        array[i] = array[i]+1;
        array[i+1] = array[i+1]+a;
        array[i+2] = array[i+2]+a;
        array[i+3] = array[i+3]+a;
        array[i+4] = array[i+4]+a;
    }
}
int main() {
    normal_loop(1);
    unroll_loop(1);
    return 0;
}
$ perf record -e branch-misses:u,branch-instructions:u ./perf_record_example
$ perf report

:u是讓 perf 只統計發生在 user space 的 event。最後可以觀察到迴圈展開前後 branch-instructions 的差距。

另外,使用 record 有可能會碰到的問題是取樣頻率太低,有些函式的訊息沒有沒顯示出來(沒取樣到),這時可以使用 -F <frequcncy>來調高取樣頻率,可以輸入以下查看最大值,要更改也沒問題,但能調到多大可能還要查一下。

$ cat /proc/sys/kernel/perf_event_max_sample_rate

參考資料

Context Switches

Context Switches 上下文切換,有時也被稱為處理程序切換(process switch)或任務切換。是一個重要的性能指標。

CPU從一個執行緒切換到另外一個執行緒,需要保存當前任務的運行環境,恢復將要運行任務的運行環境,必然帶來性能消耗。

Context Switches 上下文切換簡介

作業系統可以同時運行多個處理程序, 然而一顆CPU同時只能執行一項任務,作業系統利用時間片輪轉的方式,讓使用者感覺這些任務正在同時進行。 CPU給每個任務都服務一定的時間, 然後把當前任務的狀態保存下來, 在載入下一任務的狀態後, 繼續服務下一任務。任務的狀態保存及再載入, 這段過程就叫做上下文切換。

時間片輪轉的方式使多個任務在同一顆CPU上執行變成了可能, 但同時也帶來了保存現場和載入現場的直接消耗。

上下文切換的性能消耗

Context Switchs過高,導致CPU就像個搬運工一樣,頻繁在暫存器(CPU Register)和運行佇列(run queue)之間奔波,系統更多的時間都花費線上程切換上,而不是花在真正做有用工作的執行緒上。

直接消耗包括: CPU暫存器需要保存和載入, 系統調度器的程式碼需要執行, TLB實例需要重新載入, CPU 的pipeline需要刷掉。

間接消耗:多核的cache之間得共享資料。間接消耗對於程序的影響要看執行緒工作區運算元據的大小。

性能分析查看Context Switches的方法

Linux中可以通過工具vmstat, dstat, pidstat來觀察CS的切換情況。vmstat, dstat只能觀察整個系統的切換情況,而pidstat可以更精確地觀察某個處理程序的上下文切換情況。

最常見的,在一些排程(scheduling)演算法內,其中行程有時候需要暫時離開CPU,讓另一個行程進來CPU運作。在先佔式多工系統中,每一個行程都將輪流執行不定長度的時間,這些時間段落稱為時間片。如果行程並非自願讓出CPU(例如執行I/O操作時,行程就需放棄CPU使用權),當時限到時,系統將產生一個定時中斷,作業系統將排定由其它的行程來執行。此機制用以確保CPU不致被較依賴處理器運算的行程壟斷。若無定時中斷,除非行程自願讓出CPU,否則該行程將持續執行。對於擁有較多I/O指令的行程,往往執行不了多久,便需要讓出CPU;而較依賴處理器的行程相對而言I/O操作較少,反而能一直持續使用CPU,便形成了壟斷現象。


在 Linux 上使用 Perf 做效能分析(入門篇)

簡介

透過效能分析工具 (Profiler),我們可以得知更多關於軟體的運行資訊,像是花了多少記憶體、多少 CPU Cycles、多少 Cache Misses、I/O 處理時間等等,這些資訊對我們去找到程式效能瓶頸很有幫助。想辦法找到那裡讓程式變慢,進而最大化效能,便是我們做效能分析的最大目的。

本文將介紹 Linux 上的 perf 效能分析工具,藉由一個簡單的程式範例,示範如何使用 perf 去分析一隻程式,我們將會發現使用分析工具時能更輕易的發現問題根源。本文參考 Gabriel Krisman Bertaz 寫的 Performance analysis in Linux

本文可以搭配我的教學影片:

一個 Branch Prediction 的範例

Stack Overflow 上有一個很火的問題「Why is processing a sorted array faster than processing an unsorted array?」。

問題的 Code 如下:

test.cc

#include <algorithm>
#include <ctime>
#include <iostream>

int main()
{
    // 測試用陣列
    const int arr_len = 32768;
    int data[arr_len];

    for (int c = 0; c < arr_len; ++c)
        data[c] = std::rand() % 256;

    // std::sort(data, data + arr_len); // 是否排序
    
    long long sum = 0;

    for (int i = 0; i < 30000; ++i)
    {
        for (int c = 0; c < arr_len; ++c)
        {
            if (data[c] >= 128) { // 故意選 256 一半  
                sum += data[c];
            }
        }
    }

    std::cout << "sum = " << sum << std::endl;
}

首先我們先編譯未排序版本:

$ g++ test.cc -o unsort

接著我們把 sort 那行取消註解,再編譯一次:

$ g++ test.cc -o sort

先來看看執行時間:

$ time ./unsort
real    0m5.671s

$ time ./sort
real    0m1.932s

問題大意是說 data 如果排列過後,上面這段程式碼反而更快,如同我們實驗結果。我們知道排序的複雜度是 O(NlogN)�(�����),所以應該會比沒排直接跑的 O(N)�(�) 還慢,但結果是排序後反而更快。

就結論來說,我們知道這個結果是因為 CPU 會做 Branch Prediction。白話來說就是上次如果 iftrue,下次就先猜也是 true,CPU 可以藉由先猜來偷跑,猜對了的話就可以跑更快;但相對的猜錯的話偷跑的東西通通要丟掉,反而更浪費時間,稱為 Branch Miss(詳細原理可以參考「計算機組織」)。所以 Branch Prediction 算是一種雙面刃,如果可以一直讓條件判斷有相同結果就可以進而加速程式,反之判斷一直反覆不定就會導致一直「猜測」錯誤而變慢,因此在上面程式碼中排列過的版本反而比較快,因為猜測錯誤只會發生一次,就是在 data 剛好在 128 附近的位置,在那之前全部會是 false,而後都會是 true

Perf 效能分析工具

想要找到一段程式碼的問題通常不容易,就以上面的程式範例來說,假設我們朝演算法去分析就會走錯路,實際問題其實在計算機組織的原理。光是一段簡單的程式碼就讓我們可能找不到原因,更甭說碰到一個大的程式,裡面有各種問題存在,可能是演算法、記憶體快取、CPU 指令、網路連線、I/O 等等,這時我們需要一個分析程式來幫助我們。

Linux 上其實有很多工具可以使用:

Linux 分析工具

不過本文將針對 perf 做介紹,並用上面程式來示範假設我們還不知道問題是因為 Branch Miss,如何用 perf 找到問題。

你可以用下面指令在 Ubuntu 上裝 perf:

$ sudo apt install linux-tools-$(uname -r) linux-tools-generic

或是你也可以考慮自己從 Linux Kernel 編譯 perf 來用:

$ sudo apt install flex bison libelf-dev libunwind-dev libaudit-dev libslang2-dev libdw-dev
$ git clone https://github.com/torvalds/linux --depth=1
$ cd linux/tools/perf/
$ make
$ make install
$ sudo cp perf /usr/bin
$ perf

安裝完 perf 後,你可能會需要設定系統權限,預設應該會使 perf 權限不足:

$ sudo su # As Root
$ sysctl -w kernel.perf_event_paranoid=-1
$ echo 0 > /proc/sys/kernel/kptr_restrict
$ exit

使用 perf

接著我們要測試的程式,為了讓 perf 能用,我們要加上 -g3 參數開啟除錯模式。

一樣是編譯 test.cc,首先是沒排序:

$ g++ test.cc -g3 -o unsort

接著是編譯有排序版本:

$ g++ test.cc -g3 -o sort

Perf Record

我們現在想要知道為甚麼 ./unsort 跑得比較慢,我們可以透過 perf record 來記錄程式執行的資訊。

$ perf record ./unsort

這樣 perf 會將 ./unsort 跑的資料記錄在 perf.data 中,perf 其他指令可以用來讀取這個紀錄檔。

Perf Annotate

我們可以用 perf annotate 來看結果:

$ perf annotate

perf annotate

perf 會自動跳到花費比較多的區塊,如上圖所示,左邊是執行時間比例,右邊是程式碼對照的 Assembly Code。你可以用上下方向鍵移動,或用 h 來看操作說明。

其實從這個 Assembly 時間比例就可以看出端倪,通常我們會去看哪邊花最多時間,然後去研究背後原因。這邊的關鍵是 d8cf 這兩行,addl 其實就是在做 sum += data[c],所以這兩行分別代表 Branch Prediction 猜對和猜錯的路徑。

這張圖「箭頭」標註的是 Branch Prediction 猜對的路徑,可以看到 d8 行佔比幾乎是 0.0%。 branch prediction 猜對

這張圖「箭頭」標註的是 Branch Prediction 猜錯的路徑,可以看到 cf 行佔比幾乎是 27.7%。 branch prediction 猜錯

所以其實就可以發現整隻程式因為 Branch Misses 浪費很多時間。

這邊我們可以偷偷看一下 ./sort 的結果:

$ perf record ./sort && perf annotate

sort version's branch prediction

因為不會有 Branch Miss,可以觀察到 eef7addl 基本上沒佔多少時間。

Perf Stat

直接看 Assembly 其實滿花時間的,如果想要直接「掌握大局」,可以考慮用 perf stat

# 未排序版本
$ perf stat ./unsort
sum = 94479480000

 Performance counter stats for './unsort':

          5,671.51 msec task-clock                #    1.000 CPUs utilized
                24      context-switches          #    0.004 K/sec
                 0      cpu-migrations            #    0.000 K/sec
               147      page-faults               #    0.026 K/sec
    20,366,870,320      cycles                    #    3.591 GHz
    11,328,534,095      instructions              #    0.56  insn per cycle
     2,951,455,487      branches                  #  520.401 M/sec
       467,676,925      branch-misses             #   15.85% of all branches

       5.671777216 seconds time elapsed

       5.671781000 seconds user
       0.000000000 seconds sys

# 排序版本
$ perf stat ./sort
sum = 94479480000

 Performance counter stats for './sort':

          1,927.09 msec task-clock                #    1.000 CPUs utilized
                 6      context-switches          #    0.003 K/sec
                 0      cpu-migrations            #    0.000 K/sec
               146      page-faults               #    0.076 K/sec
     6,917,745,957      cycles                    #    3.590 GHz
    11,345,543,927      instructions              #    1.64  insn per cycle
     2,954,388,946      branches                  # 1533.084 M/sec
           268,192      branch-misses             #    0.01% of all branches

       1.927654198 seconds time elapsed

       1.927349000 seconds user
       0.000000000 seconds sys

perf stat 可以直接看到統計資料,如果有很高的 Context Switch、Page Fault、Branch Miss 都代表程式本身效能有待優化。

unsort 為例可以看到 Branch Miss 特別高 (排序版本會幾乎是 0),這時我們就可以去看原本的程式哪邊有條件判斷,然後根據 Annotate 的時間比例,就可以快速找到問題點。另外從 Cycle 上我們也可以發現兩個版本差了三倍之多。

更多 perf 的用法可以參考 Brendan Gregg 的「perf Examples」。另外這個 HackMD 的筆記也挺不錯的。

結論

本文介紹 perf 的簡單用法,用簡單的範例程式示範如何去觀察效能並找出可能的問題原因。

我們常常因為程式效能不佳而需要分析效能,但找到問題的過程往往不容易,一段程式碼效能不佳可能是演算法與資料結構的問題,可能是作業系統 System Call 導致,也可能是因為處理器架構的關係。如同本文的程式範例,說明瞭演算法的複雜度不代表真實跑出來的速度,往往還需要去考慮作業系統或是硬體架構。善用效能分析工具才能讓我們更快找到問題點。

用gcc 自製 Library

Library可分成三種,static、shared與dynamically loaded。

Static libraries

Static 程式庫用於靜態連結,簡單講是把一堆object檔用ar(archiver)包裝集合起來,檔名以 `.a' 結尾。優點是執行效能通常會比後兩者快,而且因為是靜態連結,所以不易發生執行時找不到library或版本錯置而無法執行的問題。缺點則是檔案較大,維護度較低;例如library如果發現bug需要更新,那麼就必須重新連結執行檔。

  • 編譯

編譯方式很簡單,先例用 `-c' 編出 object 檔,再用 ar 包起來即可。

  • hello.c
#include <stdio.h>
void hello(){ 
    printf("Hello "); 
}
  • world.c
#include <stdio.h>
void world(){ 
    printf("world."); 
}
  • mylib.h
void hello();
void world();
$ gcc -c hello.c world.c             /* 編出 hello.o 與 world.o */
$ ar rcs libmylib.a hello.o world.o  /* 包成 limylib.a */

這樣就可以建出一個檔名為 libmylib.a 的檔。輸出的檔名其實沒有硬性規定,但如果想要配合 gcc 的 '-l' 參數來連結,一定要以 lib' 開頭,中間是你要的library名稱,然後緊接著.a' 結尾。

使用

  • main.c
#include "mylib.h"
int main() {
    hello();
    world();
}

使用上就像與一般的 object 檔連結沒有差別。

$ gcc main.c libmylib.a

也可以配合 gcc 的 -l 參數使用

$ gcc main.c -L. -lmylib

-Ldir' 參數用來指定要搜尋程式庫的目錄,.' 表示搜尋現在所在的目錄。通常預設會搜 /usr/lib 或 /lib 等目錄。 -llibrary' 參數用來指定要連結的程式庫 ,'mylib' 表示要與mylib進行連結,他會搜尋library名稱前加lib'後接`.a'的檔案來連結。

Shared libraries

Shared library 會在程式執行起始時才被自動載入。因為程式庫與執行檔是分離的,所以維護彈性較好。有兩點要注意,shared library是在程式起始時就要被載入,而不是執行中用到才載入,而且在連結階段需要有該程式庫才能進行連結。

首先有一些名詞要弄懂,soname、real name與linker name。

soname 用來表示是一個特定 library 的名稱,像是 libmylib.so.1 。前面以 lib' 開頭,接著是該 library 的名稱,然後是.so' ,接著是版號,用來表名他的介面;如果介面改變時,就會增加版號來維護相容度。

real name 是實際放有library程式的檔案名稱,後面會再加上 minor 版號與release 版號,像是 libmylib.so.1.0.0 。

一般來說,版號的改變規則是(印象中在 APress-Difinitive Guide to GCC中有提到,但目前手邊沒這本書),最尾碼的release版號用於程式內容的修正,介面完全沒有改變。中間的minor用於有新增加介面,但相舊介面沒改變,所以與舊版本相容。最前面的version版號用於原介面有移除或改變,與舊版不相容時。

linker name 是用於連結時的名稱,是不含版號的 soname ,如: libmylib.so。通常 linker name與 real name是用 ln 指到對應的 real name ,用來提供彈性與維護性。

編譯

shared library的製作過程較複雜。

$ gcc -c -fPIC hello.c world.c

編譯時要加上 -fPIC 用來產生 position-independent code。也可以用 -fpic 參數。 (不太清楚差異,只知道 -fPIC 較通用於不同平臺,但產生的code較大,而且編譯速度較慢)。

$ gcc -shared -Wl,-soname,libmylib.so.1 -o libmylib.so.1.0.0 \
  hello.o world.o

-shared 表示要編譯成 shared library -Wl 用於參遞參數給linker,因此-soname與libmylib.so.1會被傳給linker處理。 -soname 用來指名 soname 為 limylib.so.1 library 會被輸出成libmylib.so.1.0.0 (也就是real name)

若不指定 soname 的話,在編譯結連後的執行檔會以連時的library檔名為soname,並載入他。否則是載入soname指定的library檔案。

可以利用 objdump 來看 library 的 soname。

$ objdump -p libmylib.so  | grep SONAME
  SONAME      libmylib.so.1

若不指名-soname參數的話,則library不會有這個欄位資料。

在編譯後再用 ln 來建立 soname 與 linker name 兩個檔案。

使用

與使用 static library 同。

$ gcc main.c libmylib.so

以上直接指定與 libmylib.so 連結。

或用

$ gcc main.c -L. -lmylib

linker會搜尋 libmylib.so 來進行連結。

如果目錄下同時有static與shared library的話,會以shared為主。使用 -static 參數可以避免使用shared連結。

$ gcc main.c -static -L. -lmylib

此時可以用 ldd 看編譯出的執行檔與shared程式庫的相依性

$ldd a.out
 linux-gate.so.1 =>  (0xffffe000)
 libmylib.so.1 => not found
 libc.so.6 => /lib/libc.so.6 (0xb7dd6000)
 /lib/ld-linux.so.2 (0xb7f07000)

輸出結果顯示出該執行檔需要 libmylib.so.1 這個shared library。會顯示 not found 因為沒指定該library所在的目錄,所找不到該library。

因為編譯時有指定-soname參數為 libmylib.so.1 的關係,所以該執行檔會載入libmylib.so.1。否則以libmylib.so連結,執行檔則會變成要求載入libmylib.so

$ ./a.out
  ./a.out: error while loading shared libraries: libmylib.so.1:
  cannot open shared object file: No such file or directory

因為找不到 libmylib.so.1 所以無法執行程式。有幾個方式可以處理。

a. 把 libmylib.so.1 安裝到系統的library目錄,如/usr/lib下 b. 設定 /etc/ld.so.conf ,加入一個新的library搜尋目錄,並執行ldconfig更新快取 c. 設定 LD_LIBRARY_PATH 環境變數來搜尋library這個例子是加入目前的目錄來搜尋要載作的library

$ LD_LIBRARY_PATH=. ./a.out
  Hello world.

3. Dynamically loaded libraries

Dynamicaaly loaded libraries 才是像 windows 所用的 DLL ,在使用到時才載入,編譯連結時不需要相關的library。動態載入庫常被用於像plug-ins的應用。

3.1 使用方式

動態載入是透過一套 dl function來處理。

#include <dlfcn.h>
void *dlopen(const char *filename, int flag);
//開啟載入 filename 指定的 library。
void *dlsym(void *handle, const char *symbol);
//取得 symbol 指定的symbol name在library被載入的記憶體位址。
int dlclose(void *handle);
//關閉dlopen開啟的handle。
char *dlerror(void);
//傳回最近所發生的錯誤訊息。
  • dltest.c
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
int main()  {
    void *handle;
    void (*f)();
    char *error;

    /* 開啟之前所撰寫的 libmylib.so 程式庫 */
    handle = dlopen("./libmylib.so", RTLD_LAZY);
    if( !handle ) {
        fputs( dlerror(), stderr);
        exit(1);
    }

    /* 取得 hello function 的 address */
    f = dlsym(handle, "hello");
    if(( error=dlerror())!=NULL) {
        fputs(error, stderr);
        exit(1);
    }
    /* 呼叫該 function */
    f();
    dlclose(handle);
}

編譯時要加上 -ldl 參數來與 dl library 連結

$ gcc dltest.c -ldl

結果會印出 Hello 字串

$ ./a.out
Hello

關於dl的詳細內容請參閱 man dlopen

參考資料:

Creating a shared and static library with the gnu compiler [gcc] http://www.adp-gmbh.ch/cpp/gcc/create_lib.html

Program Library HOWTO http://tldp.org/HOWTO/Program-Library-HOWTO/index.html

1、高性能程式設計關注點

1. 系統層面

  • 簡化控制流程和資料流程
  • 減少消息傳遞次數
  • 負載平衡,比如避免個別伺服器成為性能瓶頸
  • 充分利用硬體性能,比如打滿 CPU
  • 減少系統額外開銷,比如上下文切換等
  • 批處理與資料預取、記憶體屏障、綁核、偽共享、核隔離等

2. 演算法層面

  • 高效演算法降低時間和空間複雜度
  • 高效的資料結構設計,比如

C++ 資料結構設計:如何高效地儲存並操作超大規模的 76 贊同 · 7 評論文章

  • 增加任務的並行性(如協程)、減少鎖的開銷(lock_free)

3. 程式碼層面

  • I-cache(指令),D-cache(資料) 最佳化
  • 程式碼執行順序的調整,比如減少分支預測失敗率
  • 編譯最佳化選項,比如 PGO、LTO、BOLT等
  • 語言本身相關的最佳化技巧
  • 減少函數呼叫棧的深度
  • 操作放置到編譯期執行,比如範本
  • 延遲計算:(1)兩端建構(當實例能夠被靜態地建構時,經常會缺少建構對象所需的資訊。在建構對象時,我們並 不是一氣呵成,而是僅在建構函式中編寫建立空對象的最低限度的程式碼。稍後,程序再 呼叫該對象的初始化成員函數來完成建構。將初始化推遲至有足夠的額外資料時,意味 著被建構的對象總是高效的、扁平的資料結構;(2)寫時複製(指當一個對象被覆制時,並不複製它的動態成員變數,而是讓兩個實例共享動態變數。只在其中某個實例要修改該變數時,才會真正進行複製)

2、預置知識 - Cache

1. Cache hierarchy

Cache(快取)一般分為 3 級:L1、L2、L3. 通常來說 L1、L2是整合在 CPU 裡面的(可以稱之為On-chip cache),而 L3 是放在 CPU 外面(可以稱之為 Off-chip cache)。當然這個不是絕對的,不同 CPU 的做法可能會不太一樣。當然,Register(暫存器)裡的資料讀寫是最快的。比如,矩陣乘法最佳化:

寨森Lambda-CDM:C++加速矩陣乘法的最簡單方法515 贊同 · 40 評論文章img

2. Cache size

Cache 的容量決定了有多少程式碼和資料可以放到 Cache 裡面,如果一個程序的熱點(hotspot)已經完全填充了整個 Cache,那 麼再從 Cache 角度考慮最佳化就是白費力氣了。

3. Cache line size

CPU 從記憶體 Load 資料是一次一個 cache line;往記憶體裡面寫也是一次一個 cache line,所以一個 cache line 裡面的資料最好是讀寫分開,否則就會相互影響。

4. Cache associative

全關聯(full associative):記憶體可以對應到任意一個 Cache line;

N-way 關聯:這個就是一個雜湊表的結構,N 就是衝突鏈的長度,超過了 N,就需要替換。

5. Cache type

I-cache(指令)、D-cache(資料)、TLB(MMU 的 cache),參考:

https://en.wikipedia.org/wiki/CPU_cacheen.wikipedia.org/wiki/CPU_cache

3、系統最佳化方法

1. Asynchronous

非同步,yyds!

2. Polling

Polling 是網路裝置裡面常用的一個技術,比如 Linux 的 NAPI 或者 epoll。與之對應的是中斷,或者是事件。Polling 避免了狀態切換的開銷,所以有更高的性能。但是,如果系統裡面有多種任務,如何在 Polling 的時候,保證其他任務的執行時間?Polling 通常意味著獨佔,此時系統無法響應其他事件,可能會造成嚴重後果。凡是能用事件或中斷的地方都能用 Polling 替代,是否合理,需要結合系統的資料流程來決定。

3. 靜態記憶體池

靜態記憶體有更好的性能,但是適應性較差(特別是系統裡面有多個 任務的時候),而且會有浪費(提前分配,還沒用到就分配了)。

4. 並行最佳化:lock-free 和 lock-less。

lock-free 是完全無鎖的設計,有兩種實現方式:

• Per-cpu data, 上文已經提及過,就是 thread local

• CAS based,CAS 是 compare and swap,這是一個原子操作(spinlock 的實現同樣需要 compare and swap,但區別是 spinlock 只有兩個狀態 LOCKED 和 UNLOCKED,而 CAS 的變數可以有多個狀態);其次,CAS 的實現必須由硬體來保障(原子操作),CAS 一次可以操作 32 bits,也有 MCAS,一次可以修改一塊記憶體。基於 CAS 實現的資料結構沒有一個統一、一致的實現方法,所以有時不如直接加鎖的演算法那麼簡單,直接,針對不同的資料結構,有不同的 CAS 實現方法,讀者可以自己搜尋。

lock-less 的目的是減少鎖的爭用(contention),而不是減少鎖。這個和鎖的粒度(granularity)相關,鎖的粒度越小,等待的時間就越短,並行的時間就越長。

鎖的爭用,需要考慮不同執行緒在獲取鎖後,會執行哪些不同的動作。比如多執行緒佇列,一般情況下,我們一把鎖鎖住整個佇列,性能很差。如果所有的 enqueue 操作都是往佇列的尾部插入新節點,而所有的 dequeue 操作都是從佇列的頭部刪除節點,那麼 enqueue 和 dequeue 大部分時候都是相互獨立的,我們大部分時候根本不需要鎖住整個佇列,白白損失性能!那麼一個很自然就能想到的演算法最佳化方案就呼之慾出了:我們可以把那個佇列鎖拆成兩個:一個佇列頭部鎖(head lock)和一個佇列尾部鎖(tail lock),偽程式碼如下:

typedef struct node_t {
    TYPE value; 
    node_t *next
} NODE;
 
typedef struct queue_t {
    NODE *head; 
    NODE *tail;
    LOCK q_h_lock;
    LOCK q_t_lock;
} Q;
 
initialize(Q *q) {
   node = new_node()   // Allocate a free node
   node->next = NULL   // Make it the only node in the linked list
   q->head = q->tail = node   // Both head and tail point to it
   q->q_h_lock = q->q_t_lock = FREE   // Locks are initially free
}
 
enqueue(Q *q, TYPE value) {
   node = new_node()       // Allocate a new node from the free list
   node->value = value     // Copy enqueued value into node
   node->next = NULL       // Set next pointer of node to NULL
   lock(&q->q_t_lock)      // Acquire t_lock in order to access Tail
      q->tail->next = node // Link node at the end of the queue
      q->tail = node       // Swing Tail to node
   unlock(&q->q_t_lock)    // Release t_lock
}
 
dequeue(Q *q, TYPE *pvalue) {
   lock(&q->q_h_lock)   // Acquire h_lock in order to access Head
      node = q->head    // Read Head
      new_head = node->next       // Read next pointer
      if new_head == NULL         // Is queue empty?
         unlock(&q->q_h_lock)     // Release h_lock before return
         return FALSE             // Queue was empty
      endif
      *pvalue = new_head->value   // Queue not empty, read value
      q->head = new_head  // Swing Head to next node
   unlock(&q->q_h_lock)   // Release h_lock
   free(node)             // Free node
   return TRUE            // Queue was not empty, dequeue succeeded
}

具體實現可參考:高性能多執行緒佇列

Fast Concurrent Queue Algorithmswww.cs.rochester.edu/research/synchronization/pseudocode/queues.htmlwww.cs.rochester.edu/research/synchronization/pseudocode/queues.html

5. 處理程序間通訊 - 共用記憶體

關於各種處理程序間通訊的方式詳細介紹和比較,下面這篇文章講得非常詳細:

涼了!張三同學沒答好「進程間通信」,被面試官掛了....mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247485318&idx=1&sn=0da0a684639106f548e9d4454fd49904&scene=21#wechat_redirectmp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247485318&idx=1&sn=0da0a684639106f548e9d4454fd49904&scene=21#wechat_redirect

對於本地處理程序間需要高頻次的大量資料互動,首推共用記憶體這種方案。

現代作業系統普遍採用了基於虛擬記憶體的管理方案,在這種記憶體管理方式之下,各個處理程序之間進行了強制隔離。程式碼中使用的記憶體地址均是一個虛擬地址,由作業系統的記憶體管理演算法提前分配對應到對應的實體記憶體頁面,CPU在執行程式碼指令時,對訪問到的記憶體地址再進行即時的轉換翻譯。

img

從上圖可以看出,不同處理程序之中,雖然是同一個記憶體地址,最終在作業系統和 CPU 的配合下,實際儲存資料的記憶體頁面卻是不同的。而共用記憶體這種處理程序間通訊方案的核心在於:如果讓同一個實體記憶體頁面對應到兩個處理程序地址空間中,雙方不是就可以直接讀寫,而無需複製了嗎?

img

當然,共用記憶體只是最終的資料傳輸載體,雙方要實現通訊還得藉助訊號、訊號量等其他通知機制。

6. I/O 最佳化 - 多路復用技術

網路程式設計中,當每個執行緒都要阻塞在 recv 等待對方的請求,如果訪問的人多了,執行緒開的就多了,大量執行緒都在阻塞,系統運轉速度也隨之下降。這個時候,你需要多路復用技術,使用 select 模型,將所有等待(accept、recv)都放在主執行緒裡,工作執行緒不需要再等待。

img

但是,select 不能應付海量的網站存取。這個時候,你需要升級多路復用模型為 epoll。select 有三弊,epoll 有三優:

  • select 底層採用陣列來管理套接字描述符,同時管理的數量有上限,一般不超過幾千個,epoll使用樹和鏈表來管理,同時管理數量可以很大
  • select不會告訴你到底哪個套接字來了消息,你需要一個個去詢問。epoll 直接告訴你誰來了消息,不用輪詢
  • select進行系統呼叫時還需要把套接字列表在使用者空間和核心空間來回複製,循環中呼叫 select 時簡直浪費。epoll 統一在核心管理套接字描述符,無需來回複製

7. 執行緒池技術

使用一個公共的任務佇列,請求來臨時,向佇列中投遞任務,各個工作執行緒統一從佇列中不斷取出任務來處理,這就是執行緒池技術。

img

多執行緒技術的使用一定程度提升了伺服器的並行能力,但同時,多個執行緒之間為了資料同步,常常需要使用互斥體、訊號、條件變數等手段來同步多個執行緒。這些重量級的同步手段往往會導致執行緒在使用者態/核心態多次切換,系統呼叫,執行緒切換都是不小的開銷。具體實現,請參考這篇文章:

C++ 多線程(四):實現一個功能完整的線程池12 贊同 · 4 評論文章

4、演算法最佳化

比如高效的過濾演算法、雜湊演算法、分治演算法等等,大家在刷題的過程中估計都能感受到演算法的魅力了,這裡不再贅述。

5、程式碼層次最佳化

1. I-cache 最佳化

一是相關的原始檔要放在一起;二是相關的函數在object檔案裡面,也應該是相鄰的。這樣,在可執行檔案被載入到記憶體裡面的時候,函數的位置也是相鄰的。相鄰的函數,衝突的機率比較小。而且相關的函數放在一起,也符合模組化程式設計的要求:那就是 高內聚,低耦合。

如果能夠把一個 code path 上的函數編譯到一起(需要編譯器支援,把相關函數編譯到一起), 很顯然會提高 I-cache 的命中率,減少衝突。但是一個系統有很多個 code path,所以不可能面面俱到。不同的性能指標,在最佳化的時候可能是衝突的。所以儘量做對所以 case 都有效的最佳化,雖然做到這一點比較難。

常見的手段有函數重排(獲取程式執行軌跡,重排二進制目標檔案(elf 檔案)裡的程式碼段)、函數冷熱分區等。

https://github.com/facebookincubator/BOLTgithub.com/facebookincubator/BOLT

2. D-cache相關最佳化

  • Cache line alignment (cache 對齊)

資料跨越兩個 cacheline,就意味著兩次 load 或者兩次 store。如果資料結構是 cacheline 對齊的,就有可能減少一次讀寫。資料結構的首地址 cache line 對齊,意味著可能有記憶體浪費(特別是陣列這樣連續分配的資料結構),所以需要在空間和時間兩方面權衡。

  • 分支預測

likely/unlikely

  • Data prefetch (資料預取)

使用 X86 架構下 gcc 內建的預取指令集:

 #include <time.h>
 #include <stdio.h>
 #include <stdlib.h>

 int binarySearch(int *array, int number_of_elements, int key) {
         int low = 0, high = number_of_elements-1, mid;
         while(low <= high) {
                 mid = (low + high)/2;
            #ifdef DO_PREFETCH
            // low path
            __builtin_prefetch (&array[(mid + 1 + high)/2], 0, 1);
            // high path
            __builtin_prefetch (&array[(low + mid - 1)/2], 0, 1);
            #endif

                 if(array[mid] < key)
                         low = mid + 1; 
                 else if(array[mid] == key)
                         return mid;
                 else if(array[mid] > key)
                         high = mid-1;
         }
         return -1;
 }
  • Register parameters (暫存器參數)

一般來說,函數呼叫的參數少於某個數,比如 3,參數是通過暫存器傳遞的(這個要看 ABI 的約定)。所以,寫函數的時候,不要帶那麼多參數。

  • Lazy computation (延遲計算)

延遲計算的意思是最近用不上的變數,就不要去初始化。通常來說,在函數開始就會初始化很多資料,但是這些資料在函數執行過程中並沒有用到(比如一個分支判斷,就退出了函數),那麼這些動作就是浪費了。

變數初始化是一個好的程式設計習慣,但是在性能最佳化的時候,有可能就是一個多餘的動作,需要綜合考慮函數的各個分支,做出決定。

延遲計算也可以是系統層次的最佳化,比如 COW(copy-on-write) 就是在 fork 子處理程序的時候,並沒有複製父處理程序所有的頁表,而是隻複製指令部分。當有寫發生的時候,再複製資料部分,這樣可以避免不必要的複製,提供處理程序建立的速度。

  • Early computation (提前計算)

有些變數,需要計算一次,多次使用的時候。最好是提前計算一下,保存結果,以後再引用,避免每次都重新計算一次。

  • Allocation on stack (局部變數)

適當定義一些全域變數避免棧上的變數

  • Per-cpu data structure (非共享的資料結構)

比如並行程式設計時,給每個執行緒分配獨立的記憶體空間

  • Move exception path out (把 exception 處理放到另一個函數裡面)

只要引入了異常機制,無論系統是否會拋出異常,異常程式碼都會影響程式碼的大小與性能;未觸發異常時對系統影響並不明顯,主要影響一些編譯最佳化手段;觸發異常之後按異常實現機制的不同,其對系統性能的影響也不相同,不過一般很明顯。所以,不用擔心異常對正常程式碼邏輯性能的影響,同時不要借用異常機制處理業務邏輯。現代 C++ 編譯器所使用的異常機制對正常程式碼性能的影響並不明顯,只有出現異常的時候異常機制才會影響整個系統的性能,這裡有一些測試資料。

另外,把 exception path 和 critical path 放到一起(程式碼混合在一起),就會影響 critical path 的 cache 性能。而很多時候,exception path 都是長篇大論,有點喧賓奪主的感覺。如果能把 critical path 和 exception path 完全分離開,這樣對 i-cache 有很大幫助

  • Read, write split (讀寫分離)

偽共享(false sharing):就是說兩個無關的變數,一個讀,一個寫,而這兩個變數在一個cache line裡面。那麼寫會導致cache line失效(通常是在多核程式設計裡面,兩個變數在不同的core上引用)。讀寫分離是一個很難運用的技巧,特別是在code很複雜的情況下。需要不斷地偵錯,是個力氣活(如果有工具幫助會好一點,比如 cache miss時觸發 cpu 的 execption 處理之類的)

以C++為核心語言的高頻交易系統是如何做到低延遲的?982 贊同 · 31 評論回答

6、總結

上面所列舉的大多數還是通用的高性能程式設計手段,從物理硬體 CPU、記憶體、硬碟、網路卡到軟體層面的通訊、快取、演算法、架構每一個環節的最佳化都是通往高性能的道路。軟體性能瓶頸定位的常用手段有 perf(火焰圖)以及在 Intel CPU 上使用 pmu-tools 進行 TopDown 分析。接下來,我們將從 C++ 程式語言本身層面出發,探討下不同場景下最高效的 C++ 程式碼實現方式。


減少上下文切換(context switch)

在作業系統中,減少上下文切換(context switch)的方法有幾種:

  • 批處理任務:將相關任務批次處理,減少任務之間的頻繁切換。這意味著,一旦一個任務開始執行,它會執行一段時間,而不是在短時間內多次切換到其他任務。

  • 多執行緒和協程:使用執行緒池或協程來避免建立過多的執行緒或處理程序。執行緒和處理程序的切換開銷很高,但協程可以在同一個執行緒內進行切換,開銷更小。

  • I/O 多路復用:使用像 epoll(在 Linux 上)或 kqueue(在 BSD 上)這樣的機制,允許一個處理程序監視多個檔案描述符上的事件,從而減少了不必要的上下文切換。

  • 減少中斷處理:在核心中儘可能減少中斷處理的時間,以減少被打斷的頻率。這可以通過最佳化中斷處理程序的程式碼來實現。

  • 最佳化調度演算法:調度演算法的最佳化可以減少不必要的上下文切換。例如,使用搶佔式調度演算法來確保高優先順序任務盡快執行。

  • 共用記憶體:通過共用記憶體而不是複製資料來減少處理程序之間的通訊開銷。這可以減少由於處理程序通訊而導致的上下文切換。

  • 避免競爭條件:競爭條件可能導致頻繁的上下文切換,因此設計時應儘量避免這種情況的發生。

總的來說,減少上下文切換的關鍵在於合理設計程序結構和演算法,以及最佳化作業系統的調度和資源管理策略。

你還在用老派網格交易嗎? 試試新一代的 bollmaker 策略吧

網格交易

其實經歷 2020 年到 2022 年這整整兩年的加密貨幣牛市,很多人應該都已經知道網格交易策略是什麼。

熟悉網格交易的朋友應該都知道,網格需要設定區間,所以,要不是你的區間設定得非常好,每次抓震盪區間都能夠抓到非常準確,就是你必須砸重金開張天地網格,設下非常大的區間和非常多的格數。 但是天地網格單其實資金利用率很低,因為震盪幅度每天可能 2%~5% 左右遊走,你的區間如果設定是 500% 的上下區間,那可能每天吃到的單就很有限,掛單只能吃到很小的範圍。

但是在牛市中,震盪區間常常接下來就是幅度不小的拉盤,如果你設定震盪區間網格,那麼每次價格突破你的震盪區間,網格就是一定會幾乎賣飛所有現貨。

研發全新穿越牛熊交易策略 — bollmaker

筆者這段時間一直在研究交易,在去年第二波新高回落時,思考什麼樣的策略有辦法穿越牛熊,也因此有了一些交易策略的靈感。

img

2021 年末 ETHUSDT 走勢

如上圖,ETHUSDT 的 4h 小時線做肉眼回測,當時是在接近 ATH 4800 左右的位置,價格不斷在布林通道上下方震盪來回作出緩跌,每一次都會重新回到布林通道上方,才繼續開始下跌。

所以筆者利用布林通道加上部位控制的算法,研發出 bollmaker 這套使用雙重布林通道的造市策略,但沒想到效果意外地好,只要設定完,放著給他跑就可以應付各種價格區間,幾乎不用一直調整參數。

雖然是自己研發,又是開放原始碼,肯定會有人抄襲模仿,但是筆者不怕,原因是我們每週都持續在研發改進。

我們先來看 bollmaker 的回測績效:

img

使用八月的資料來做回測,測試到九月初的超大暴跌,初始資本: 20,000 USDT+ 1 ETH,單月已實損益 +$7,326.69。

最關鍵的是,跑完策略後,維持未實現損益 UNREALIZED PROFIT: +$ 17.55

沒有過度 hold ETH 部位,也沒有提早賣飛 ETH 部位,如果用這個獲利來試算 APY 的話,約是 APY +3097%。

bollmaker 實戰的下單位置

先來看筆者跑 bollmaker 實戰的下單位置,這是幣安 App 的下單歷史顯示,S 是 Sell 賣單成交,B 是 Buy 買單成交:

img

img

img

bollmaker 的基本原理

其實 bollmaker 顧名思義就是他算是一種造市策略,透過利用在市場上創造流動性,同時在 mid price 上下方掛買單賣單,被動等其他人下單跟你成交來取得獲利。

但造市策略的天敵就是一路跌和一路漲,所以 bollmaker 透過兩個不同時間區間的布林通道來管控造市的下單。

長時間通道: 用來控制部位大小

短時間通道: 用來避免在強趨勢出現的時候下單

img

用上圖來解釋,如果我們長時間通道採用 30m,畫出來的布林通道大概是長這樣,價格根據常態分佈的機率 95.4 會在通道內遊走,因此我們抓下方通道用來囤貨,上方通道用來出貨來掛單,並依照通道的寬度去算出一個最大部位的分佈。

舉例來說,當價格靠近布林通道上方的時候,我們可以設定最多買 ETH 就是買到 0.1 顆,當價格靠近布林通道下方的時候,我們可以設定最多買 ETH 可到 10 顆。

所以透過這樣價格的遊走和動態的調控部位,我們就可以利用市場上的 Spread 價差來買低賣高獲利。

bollmaker 是利用向下攤平的概念,在下跌趨勢中降低持有成本,並在反彈拉回的時候有機會能夠賣出並減碼,因此只要你本金足夠,就有辦法在市場週期中賺到錢。

BollMaker 適合的市場和情境

基本上要跑 bollmaker 主要還是以囤貨為目的,囤貨和逃頂是兩個極端的面相,兩者很難用同一組設定兼得。 也因此,你需要去選擇一個你願意長期投資的資產來做操作,不要去選投機幣來跑。

所以當你覺得現在市場行情可以準備開始囤貨,就很適合跑 bollmaker 策略。

建議跑在市值相對高的幣種,成交量高,震盪也多,且因為市值高的幣種在空頭市場來的時候,跌幅也不會像很多小幣一樣這麼深。大幣跌 8% 的時候,有些小幣一天可以跌到 20%。

BollMaker 策略的缺點

醜話也是要先講在先,畢竟沒有策略能夠應付所有市場的極端情況。

bollmaker 不怕急跌,因為這樣反而有機會在短時間內拉低持倉成本,但 bollmaker 的天敵是一路緩跌完全沒有回彈 — 為什麼?因為價格要靠近布林通道上方的時候,我們才有機會開始減倉。

bollmaker 在掛單的時候,可採取每分鐘掛單一次,或是每五分鐘掛單一次,這可以自由設定,時間越短,成交頻率越高,獲利也會相對可觀,但在面對下跌的狀況,自然就會犧牲逃頂的特性。

剛剛上面有講到,bollmaker 是利用向下攤平的概念,在下跌趨勢中降低持有成本,並在反彈拉回的時候有機會能夠賣出並減碼,所以如果你的本金只有幾百美金是沒辦法跑的 — 為什麼? 因為交易所都有最小下單金額限制,通常是十美金,你每次掛單至少需要 10 美金,但你只準備 100 美金的話,就只有 10 次機會可以向下攤平,所以這樣會導致套牢的機率非常大。

根據筆者的實測,本金最少最少要能夠做 100~200 次以上的掛單,每單金額至少要 20U,也就是本金差不多需要 200 x 20 = 4000 是最小需求。

市場價差 Spread

由於 bollmaker 的獲利靠的是市場的波動性,所以交易所的主動成交波動性很重要,看的是市場上的 taker buy 和 taker sell。

價格波動性來講,Binance 還是比 FTX 高,如果希望成交頻率高的話,會比較推薦在幣安上面跑 bollmaker。 FTX 也不是不行,但頻率自然會低一點。

在 MAX 交易所的話,由於 MAX 交易所的手續費非常低,幾乎是幣安的一半以上,而且又特別容易升等 VIP,所以如果你要在 MAX 上跑可以掛更近,舉例來說幣安上如果掛 0.1% 的價差 (幣安手續費是 0.075%),那在 MAX 上可以掛到 0.05% 的價差達到類似的效果。

如果你喜歡筆者開發的自動交易策略,歡迎給筆者一點支持和鼓勵,可使用筆者的推薦碼註冊幣安, MAX 或 FTX 🙏

  • 幣安 https://accounts.binance.com/en/register?ref=38192708
  • MAX https://max.maicoin.com/signup?r=c7982718
  • FTX https://ftx.com/referrals#a=bbgo

如何設定 bollmaker?

如果你是工程師,對怎麼 build & install 已經非常熟悉了,可以直接參考 bbgo 官方 repository 的文件做設置:

https://github.com/c9s/bbgo

如果你都還不懂,也還沒有跑過 bbgo ,可以先看看下面這篇:

BBGO — 在家也可以跑網格策略程式各位從 Google 進來看到這篇文章,應該已經很暸解網格策略的運作方式還有獲利模型,網格策略也有很多種變化,在這邊就不再闡述。c9s.medium.com

如果各位已經 ready 了,那就可以開始來設定 bollmaker 了。 先來看筆者建議的設定:

---
persistence:
  redis:
    host: 127.0.0.1
    port: 6379
    db: 0

exchangeStrategies:
- on: binance
  bollmaker:
    symbol: BNBUSDT
    interval: 1m
    amount: 30
    askSpread: 0.1%
    bidSpread: 0.1%
    minProfitSpread: 0.2%
    useTickerPrice: true
    
    dynamicExposurePositionScale:
      byPercentage:
        # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale
        exp:
          # from down to up
          domain: [ -1, 1 ]
          # when in down band, holds 10.0 by maximum
          # when in up band, holds 1.0 by maximum
          range: [ 10.0, 1.0 ]

    uptrendSkew: 0.2
    downtrendSkew: 1.5

    long: true
    buyBelowNeutralSMA: true
    tradeInBand: true

    defaultBollinger:
      interval: "4h"
      window: 21
      bandWidth: 2.0

    neutralBollinger:
      interval: "5m"
      window: 21
      bandWidth: 2.0

    persistence:
      type: redis

以上,我們先看到 persistence 有設定 redis。 bbgo 基本上要執行是可以不用 redis 的,但是因為我們需要紀錄部位資訊,重開 bbgo 之後也要能夠載入回來,所以這邊要設定一下 redis,沒有的話,可以裝一下:

apt install redis

繼續往下看到 exchangeStrategies 的部分,我們設定了 bollmaker ,設定細節如下:

interval 是你多久要下單一次, 1m 就是一分鐘,可使用的參數有 1m , 5m , 15m , 30m , 1h 等等,當然如果你使用越慢的掛單頻率,可以透過增加每次掛單金額來提高獲利率。

amount 是你每次的下單金額 (BNBUSDT 的話,是以 USDT) 計算,這邊寫 30,意味著我們下單的顆量是 $30 等值的 BNB 顆量 (Quantity)。 剛剛有講到,這邊的 amount 最少是你的本金 1/100 或甚至 1/200, 最好可以到 1/500。

spread 是掛單距離 mid price 的價差,所謂的 mid price 是 (第一檔賣單價格 + 第一檔買單加格) / 2。 通常設定 0.1% 左右,依照你的手續費比率來設定。 幣安是 0.075% ,因此我們這邊設定 0.1%。 如果是使用 MAX 交易所的話,可以設定 0.06% 左右。 spread 的設定跟 interval 參數有關,你可以打開線圖看每個分線的震幅 (Amplification),只要小於這個震幅,成交頻率就會夠高。

long 是指我們的 bollmaker 只做多頭部位 (只增加持倉,不會一開始就減倉),不會賣掉你原本帳號裡手動買的部位。

minProfitSpread 是指,當目前價格比持有成本高出 minProfitSpread 後,我們才開始掛賣單,這個數值越小越可以避險,但是在牛市也就越容易賣飛。

buyBelowNeutralSMA 是指,當價格在布林通道的 SMA 以下我們才開始掛買單,這個設定是用來應付空頭市場,如果在牛市,會建議不要啟用 (可改為 false)。

dynamicExposurePositionScale 就是我們用來調控部位的設定了,其中你看到 [-1, 1] 請不要去改他,你要修改的是 range ,-1 對應的就是布林通道下方, 1 對應的是布林通道上方。 也就是,如果設定 [100, 1] ,那麼你在布林通道下方最多就是買到 100 顆,上方最多買到 1 顆。 (這個部分的設定你可以一邊運行 bbgo 一邊依照需求修改,改完只要重啟 bbgo 即可)

defaultBollinger 這個就是用來控制部位的布林通道,詳細設定你可以用 TradingView 或是幣安的走勢圖選一個合適布林通道出來用,這邊建議不要抓太近抓到 5m 或甚至 15m 都太短。

neutralBollinger 這個是用來偵測短期趨勢用的布林通道,加上 tradeInBand 這個選項,如果超過這個布林通道的區間,bollmaker 就會暫停掛單。

最後,存檔好你的 bbgo 設定,就可以起飛:

bbgo run

如果懶得自己設定,有在用 Linode 的朋友也可以用筆者準備的 StackScript 做一鍵部署:

Linode StackScript — BollMakerbbgo bollmaker deploymentcloud.linode.com

☝☝☝ ️️️️Linode StackScript for BollMaker

最後~

如果你喜歡筆者開發的自動交易策略,歡迎給筆者一點支持和鼓勵,可使用筆者的推薦碼註冊幣安, MAX 或 FTX 🙏

  • 幣安 https://accounts.binance.com/en/register?ref=38192708
  • MAX https://max.maicoin.com/signup?r=c7982718
  • FTX https://ftx.com/referrals#a=bbgo

出處 https://c9s.medium.com/bbgo-bollmaker-%E7%A9%BF%E8%B6%8A%E7%89%9B%E7%86%8A%E7%9A%84%E8%87%AA%E5%8B%95%E4%BA%A4%E6%98%93%E7%AD%96%E7%95%A5-b573ba6625b3

淺析經典高頻做市策略

引言:做市策略指的是一種分別建立限價買賣單,利用標的價格的上下波動觸發限價單,通過買賣單的差價獲取交易收益的策略。做市策略中重點關注的是限價單的數量和以及買賣單報價與中間價距離的設定,因而在各類經典的做市策略中,主要研究的是中間價的估算,進而在中間價兩邊合適的位置設定買單和賣單。本文將介紹做市的基本概念以及兩大經典高頻做市策略。

第一章:做市的基本概念

在一些流動性較好的標的資產的交易活動中,普通投資者可以通過提交市價單或者直接買賣標的資產或者相關衍生標的直接參與市場交易。這樣的市場中存在較多的投資者,資產流動性好。只要投資者在合理的價位出價,很快就可以找到交易對手。通過連續不斷的交易活動,資產的價值也可以逐漸反映在價格中,從而使得標的資產的價值得到充分的體現。這一方面促進了標的資產的價值實現,另一方面推動了市場整體資金的流動,一定程度上提高了市場投資活動的積極性,為市場經濟注入活力。

但在一些流動性較差的資產上,由於種種原因,參與這些資產投資活動的交易者較少,想要對這些資產展開買賣活動的投資者很難正確認識到資產的真實價值,也很難找到合適的對手進行成交,這時候就需要藉助做市商來為市場提供流動性了。

1.做市的定義

什麼是做市?簡而言之,做市就是製造市場流動性。股票市場的驅動方式分為報價驅動和訂單驅動。在訂單驅動的市場上,做市商有幾種不同的形式:交易所可以與指定做市商達成協議,在保證市場公平有序的前提下提供更多買賣報價,指定做市商必須在開盤和收盤時參與競價並在規定時間內保證一定比例的報價。非交易所指定的做市商則不必承擔上述義務,只是作為流動性提供者。採用做市策略的高頻交易者便是如此,在高頻交易資料的基礎上圍繞標的資產的價格在不同價位掛出限價單,當標的資產價格在不斷波動時會觸發做市商掛出的低價買單和高價賣單,做市商借此實現低買高賣,並賺取差價。而在報價驅動的市場上,做市商就是交易商,他們提供通過報價向其他市場參與者提供流動性。做市商通過賺取買賣價差獲利,每個做市商就像一個小的交易所。

2.做市策略的收益來源

由此可見,做市商在依賴於資產價格波動中高低價之間的差價獲得收益。那麼,這種買賣價差是如何形成的?Harold Demsetz, 1968研究了紐約股市的交易成本,研究中首次闡述了做市商買賣報價差的形成過程:供求的不平衡會導致價差產生,“買賣報價價差是有組織的市場為交易的即時性(immediacy)支付的加成”。做市策略通常在雙邊報價,通過成交價格在價差間的窄幅波動獲利,而這裡的窄幅一般只有1-2個價位,而非大方向性變化。根據市場有效理論,股票價格在市場有效的狀態下為“隨機遊走”,價格的走向不可預測。然而長期跟蹤研究發現,價格的長期走勢具有“均值回歸(Mean Reversion)”的特點。均值回歸在理論上具有必然性,價格走勢不可能只升不降或者只降不升,價格保持正收益率或負收益率稱之為均值迴避(Mean-aversion)。在均值回歸理論中,均值迴避的現像是暫時的,均值回歸是必然的。資產價格偏離其內在價值的程度影響均值回歸週期的長短。

Tanmoy Chakraborty and Michael Kearn, 2011通過理論和公式推導,進一步明確了做市策略的絕對收益。假設所有的市場事件出現在離散的時間點位 0,1,2 直到時刻T,在收盤時刻 T ,做市策略必須平掉所有的單方向淨頭寸。文章中證明瞭做市策略的理論收益為12(K−z2),其中K=∑t=1T|Pt+1−Pt|表示價格波動的絕對幅度, z=PT−P0 表示收盤後平掉淨頭寸產生的淨盈虧。研究也進一步證明瞭在均值回歸的條件下,該理論收益的期望為正,也就是說在均值回歸的假設下,做市策略確實可以產生絕對收益。通過這個理論收益公式,我們也可以發現做市商需要儘可能捕捉價格的窄幅波動,而清除庫存以減低庫存風險的操作則會對做市商的絕對收益產生一定削減效應。

3.做市v.s.統計套利

在做市中,做市商不總是提交最優價格的買單或賣單,並且會儘量保持買賣單的平衡,減少庫存風險。由於庫存風險的存在,即做市商的庫存端可能會面臨反向的上漲趨勢,從而使其擔憂在標的資產上的淨空頭或淨多頭暴露,此時做市商會盡快做出反方向的買賣單從而降低庫存風險。另外有時為了防止買賣單被打穿,做市商也會在更深的價位做單。而統計套利策略的投資者則會故意做出方向性的交易,直到標的價格回歸到合理價格區間,因此在套利操作中存在在某個標的上的淨多頭或空頭是十分常見的。當然,做市商和套利交易者都持有中長期趨勢回歸的觀點——做市商在中間價基礎上在兩端建立買賣單,認為標的資產的價格會在中間價兩側上下波動,從而觸發買單和賣單,而做市商從中賺取差價;而套利投資者則是從標的資產的相對價值或者絕對價值出發,認為一定時間內,價格會回歸到相對價值或者絕對價值區間,因此當套利投資者發現某標的資產價格或者某組標的的相對價值脫離合理區間時,他們就會通過淨持倉,等待價格回歸到合理的價值區間,從而獲得策略收益。

4.做市策略的風險管理

在做市中,主要包括三類成本——指令處理成本、存貨成本、資訊成本,一般交易所指定的做市商會得到一定比例的手續費返還,因此做市商在建構做市策略中常常關注後兩種交易成本,在此基礎上,形成了兩類做市模型——存貨模型和資訊模型。

(1)指令處理成本

指在交易指令發生時做市商所承擔的如印花稅、過戶費等成本(早期還包含人工下單的人力成本)。

(2)存貨成本

指做市商在向市場提供買賣報價時而保有一定數目的證券等頭寸而產生的成本。因為標的物的價值會不斷變動及價格和交易數量的不確定性,有可能帶來的成本損失。通常來說,交易量的不確定性越高則做市商保持的存貨越多;標的物的價格不確定性越高,存貨成本則越高。做市商期望通過買賣價差來獲取利潤。做市商如果不同時買賣,就會產生庫存成本。做市商面臨的最核心問題就是如何在有多空庫存暴露的風險下更加精準地報價,避免積累大量的多空頭寸(圖1)。Smidt, 1971指出被動報價的做市商假設不能夠解釋價差隨著股票價格的波動而變化,做市商作為市場的理性參與者,也不會被動地進行報價,而是根據自己的庫存情況主動地調整報價,控制庫存風險和提高庫存的周轉率。

img

圖1:存貨模型對於買賣報價的調整(來源:加密資產衍生品新藍海,期權交易詳解,2020.)

做市商往往根據收益最大化和風險最小的原則來決定買賣報價,做市商控制自身的初始財富以及任一給定時間點手中所持有的資金和存貨。Garman , 1976首次提出了基於庫存的做市商報價決策模型。此後,庫存模型從單個做市商的情況發展至多個做市商的情形(Ho and Stoll, 1983),由單期模型擴展至多期決策,並引入了風險係數來研究不同風險偏好程度的做市商的決策(Stoll, 1978, Ho and Stoll, 1981)。但Madhavan and Smidt, 1991也通過實證表明庫存風險並不能完全解釋對做市商的收益。

(3)資訊成本

資訊成本是未知情交易者對知情交易者付出的成本。如果在市場上存在資訊知情者,那麼在這種資訊不對稱的場景下,做市商如果選擇和資訊知情者交易,將會承擔一定的損失。Treynor, 1971指出在交易中,做市商根據委託訂單的情況來區分具備和不具備資訊優勢的交易者的交易動機對交易決策有重要的作用。因此,如何通過對訂單流的分析來獲取隱藏的交易資訊,成為做市商報價模型的新的發展發向。Stoll, 1989利用NASDAQ資料將市場實際的交易價差分解為訂單處理成本(45%)、庫存成本(10%)和資訊不對稱成本(45%),發現資訊不對稱成本和訂單處理成本是影響做市商行為的最重要的兩個因素。

Bagehot, 1971提出資訊模型,當下隨著交易資訊公開化和電子交易的推動,投資者大量使用限價單。做市商也會分析市場微結構,研究訂單簿波動性來預測價格的短期變化。


第二部分:經典高頻做市策略

接下來,我們將簡單介紹兩大經典的高頻做市模型——AS模型(Avellaneda, M., and S. Stoikov, 2008)和GP模型(Fabien Guilbaud and Huyen Pham, 2011)。

經典高頻做市策略之一:AS模型

Avellaneda, M., and S. Stoikov, 2008在庫存風險管理的基礎上建立了高頻做市的AS模型。AS模型的理論基礎源於Ho and Stoll, 1980和Ho and Stoll, 1981這兩篇文章的研究結論,前者分析了在競爭環境中,做市商的報價與所有代理商的無差別報價相關;而後者則研究了一個做市商在考慮了存貨風險的前提下,單項標的資產報價中的最優決策——即在資產的“真實價格”兩側建立最優買賣單。AS模型在此基礎上,研究了市場中單個做市商的最優決策行為,並用市場中間價代表所謂的“真實價格”。模型建立主要分為兩個步驟:首先,做市商在給定庫存下,計算出自身對資產的無差異估值,即中間價格;其次,根據報價單與中間價之間的距離推算報價單被執行的機率,在此基礎上結合市場環境和做市商的風險承受能力建立效用函數,推匯出做市商的最優報價。

第一部分:模型推導

(1)中間價格

做市商對於標的資產的無差異估值由下式給出:

dSu=σ∗dWu

中間價格的初始值St=s,上式中Wu表示一維標準布朗運動。

(2)效用函數

做市商的目標是為了在時間T實現損益最大化,為了研究做市商的效用函數,Avellaneda, M., and S. Stoikov首先以不活躍的交易者為例考察了做市商的效用函數。

不活躍的交易者指的是尚未提交報價任何報價單,在投資期間標的資產上有固定持有庫存q的投資者。假設該交易者原來持有現金x,當其庫存為q時,該投資者的效用函數為v,使用凸函數度量風險,此時交易者的效用函數如下式所示:

v(x,s,q,t)=E[−exp⁡(−γ(x+q∗ST)]⇒v(x,s,q,t)=−exp⁡(−γ(x+q∗s))∗exp⁡(γ2q2σ2(T−t)2)

對於不活躍的交易者,當他們願意以一單的價格買入一單位標的資產,成交後該交易者持有現金x−rb,庫存增加一單位,若此交易行為對該交易者的效用不產生影響,則rb表示該交易者的無差異買價(reservation bid price),即rb應當滿足:

v(x−rb,s,q+1,t)=v(x,s,q,t)

通過同樣的方式我們也可以建立無差異賣價(reservation ask price)ra的等量關係式,解得:

ra(s,q,t)=s+(1−2q)γσ2(T−t)2rb(s,q,t)=s+(−1−2q)γσ2(T−t)2r(s,q,t)=s−qγσ2(T−t)

其中r(s,q,t)表示買賣價的均價。

上述討論是針對有限的投資期間T−t展開的,若從無限的時間長度來討論,該投資者的效用函數為

v¯(x,s,q)=E[∫0∞v(x,s,q,t)dt]ω=γ2q2σ22⇒v¯(x,s,q)=E[∫0∞−exp⁡(−ωt)∗exp⁡(−γ(x+qSt))dt]

其中ω將決定投資者允許持有庫存量的上界,一般設定為

ω=12γ2σ2(qmax+1)2

(3)建構限價單

為瞭解決最優報價決策的問題,模型進一步研究了可以通過限價單交易參與市場投資的做市商的行為。

A.限價單報價及執行數量

做市商在中間價兩端分別以pa和pb的價格分別報單,假設做市商可以連續無成本報價,報價單與中間價之間的距離δa=pa−s、δb=pb−s,以及當前限價單的結構決定了該做市商限價單被執行的優先順序。具體來說,以限價買單為例,若市價賣單數量為Q,當這批賣單最深的價位pQ低於做市商限價買單報價pb時, 限價單被擊穿成交。而實證研究表明市價賣單最深價位與中間價的差價Δp與市價賣單數量的對數值成正比,即

Δp=pQ−s∝ln⁡(Q)

經過時間t後,做市商分別持有Nta手空單,Ntb手多單。根據研究,假設Nta、Ntb分別服從速率為λa和λb的泊松過程,λa和λb表示限價單分別被市價點選穿的機率,當δ超出Δp時,限價單將不會被擊穿,得到λ(δ)=Aexp(−kδ) 。

B.最佳化問題

經過時間t後,做市商持有現金Xt,滿足

dXt=padNta−pbdNtb

淨庫存為qt=Ntb−Nta。此時做市商面臨的最佳化問題是:

u(s,x,q,t)=maxδa,δbE[−exp⁡(−γ(XT+qTST))]

上述等式也需要同時滿足

u(x,s,q,t)=u(x−rb,s,q+1,t)=u(x+ra,s,q−1,t)

我們可以通過求解上述價值函數的Hamilton-Jacobi-Bellman偏微分方程解得 ra 和 rb 的均值即中間價以及 δa 和 δb 的和:

r(s,q,t)=ra+rb2=s−qγσ2(T−t)δa+δb=2γln⁡(1+γk)

第二部分:實證研究

img

圖2.1:圍繞中間價格進行報價的做市收益(來源:華泰期貨研究院)

img

圖2.2:使用AS模型的做市收益(來源:華泰期貨研究院)

華泰期貨,190425對比了圍繞中間價進行報價(圖2.1)和參照AS模型圍繞無差別價格進行報價(圖2.2)這兩種方式進行做市的策略收益,可以發現兩種方法在賬面上都能產生盈利,但AS模型的策略收益要顯著高於直接圍繞中間價進行報價的策略收益,並且前一種方法在限倉後明顯產生了收益的下滑,而AS模型的收益則在限倉前後持續上漲。從返傭比例的臨界點來看(圖3),使用AS模型僅需要57.22%的手續費返還就可以實現盈虧平衡,而直接在中間價兩端進行報價則需要返還84.51%的手續費才能扭虧為盈。

img

圖3:策略收益對比(來源:華泰期貨研究院)

第三部分:拓展與推廣——ASQ模型

華泰期貨,190520指出AS模型能有效模擬市價單的成交情況,但是沒有對庫存風險進行有效管理,因此該研究在AS模型模型的基礎上加上了對庫存最值的限制,當庫存達到最值時,立即停止相應端的報價,只做反方向報價,以期減少庫存,降低庫存風險。通過實證結果的對比(圖4.1、圖4.2),我們可以發現,AS模型和加上庫存限制的ASQ模型策略收益幾乎相當,但ASQ模型下手續費用更低,也因此使得ASQ模型對返傭比例的要求更低,最大持倉也更小。

img

圖4.1:AS模型v.s.ASQ模型做市策略收益(來源:華泰期貨研究院)

img

圖4.2:策略收益對比(來源:華泰期貨研究院)


經典高頻做市策略之二:GP模型

Fabien Guilbaud and Huyen Pham, 2011根據動態規劃原理在不同的庫存條件以及成交機率下,在一檔掛限價單,對比以最優買價、最優買價+一個跳價,最優賣價、最優賣價+一個跳價這四種掛單方式中的最優選擇。

GP模型假設做市商的目標是通過市價單和限價單,控制庫存數量,實現在某個短期區間的收益最大化。通過Markov過程模擬中間價的變動,使用Cox過程模擬給定價差和限價時做市商的限價單成交情況,並結合Calibration程序估計轉移矩陣和價差的密度參數等,最終形成一個以庫存和價差變數為基礎執行的動態作業系統。

第一部分:模型推導

(1)中間價格和價差

GP模型使用外生的Markov過程來模擬標的資產中間價的變化。

對於使用限價單參與交易的投資者而言,隨機買賣價差是市場參與者的投資結果,會在隨機的時間點發生跳動,並且這些價差只能是單個跳價的整倍數,因此研究中使用Cox過程模擬價差的變動,即分別使用獨立的泊松過程模擬市場上的買價和賣價,並通過實際資料估計價差的轉移矩陣:

ρij:=P[Sn+1=jσ∣Sn=iσ]ρij:=∑n=1K1{(S^n+1,S^n)=(jσ,iσ)}∑n=1K1{S^n=iσ}

(2)建構限價單

在價格選取的過程中,做市商需要在當前的最優報價或者相對最優報價差一個跳價的報價之間做出抉擇,後者在實際應用中主要是為了提高將當前限價單在限價單序列中的位置提前,從而提高該限價單被執行的機率。在限價單建構這一步中,做市商的限價單策略可以被描述為

αtmake=(Qtb,Qta,Ltb,Lta)

其中L=(La,Lb)表示的限價單量,Q=(Qa,Qb)表示限價單報價決策。對於限價買單,做市商可以選擇最優買價,或者最優買價+一個跳價,即

Qb=(Bb,Bb+),Bb+=Bb+tick

同樣的,做市商也可以選擇在最優賣價或者最優賣價-一個跳價上報出限價單,即

Qa=(Ba,Ba−),Ba−=Ba−tick

在以下幾種特殊情況下,做市商的限價單報價是確定的:

A.初始報價時:直接選擇最優報價,即

Qb=Bb,Qa=Ba

B.當差價為一個跳價時:直接選取最優報價,即

Qb=Bb,Qa=Ba

由於此時差價為一個跳價,Bb+=Ba ,Ba−=Bb。

根據之前的研究,我們已經得到了中間價和價差,當進一步確定了選取最優報價還是次優報價(即比最優報價差一個跳價的報價)時,做市商就可以報出限價單的價格π(q,p,s),當價差 s=i∗δ 時,定義

πi(p,q)=π(q,p,i∗δ)

接下來,使用Cox過程模擬限價單被執行的過程,即分別用獨立的泊松過程Na和Nb表示市價買單和市價賣點選中限價單,這兩個過程的密度參數分別為λa(Qta,St)和λb(Qtb,St)。相應地,我們可以建構出庫存Y和現金X的微分方程:

dYt=Ltb∗dNtb−Lta∗dNtadXt=−πb(Qtb,Pt−,St−)Ltb∗dNtb+πa(Qta,Pt−,St−)Lta∗dNta

(3)利用市價單減少庫存

為了降低庫存風險,GP模型也通過引入市價單來減少庫存,市價單部分的決策包括市價單下單量和執行時間,

αtake=(τn,ζn)

其中,τn表示市價單的執行時間序列,ζn則表示每次市價單執行時觸發的單量,ζn>0 表示在最優賣價買入,ζn<0表示在最優買價賣出,那麼隨之產生的庫存Y和現金序列X分別滿足:

Yτn=Yτn−+ζnXτn=Xτn−−c(ζn,Pτn,Sτn)c(e,p,s)=ep+|e|∗s2+ε

其中c表示做市商執行市價單的成本,ε表示固定費用。

(4)最佳化

綜合上述步驟,做市商的目標是通過限價單和市價單為基礎的綜合做市策略α=(αmake,αtake),實現效用函數最大化:

maxE[U(L(XT,YT,PT,ST))−γ∫0Tg(YT)dt]L(x,y,p,s)=x−c(−y,p,s)=x+yp−|y|s2−ε

其中 L 表示做市商持有的流動性函數,U表示做市商持有的流動性為其帶來的效用,庫存則在效用函數中則表現為懲罰,從而使得效用函數在達到最優時,把庫存風險降低到可容忍範圍內。

在Fabien Guilbaud and Huyen Pham的研究中,他們考察了兩種方式來度量流動性為做市商帶來的效用:U(x)=x;U(x)=−exp(−ηx),並通過分離變數的方法將整體效用函數的最佳化問題簡化為

U(x)=x,g(y)=y2vi(t,x,y,p):=v(t,x,y,p,s=iδ)vi(t,x,y,p)=x+yp+ϕi(t,y)

同樣通過求解上述效用函數的Hamilton-Jacobi-Bellman偏微分方程,可以得出做市商的最優策略。研究發現在兩種效用函數下,最優決策均與市場價格無關,與庫存水平和價差相關。

第二部分:實證研究

華泰期貨,190730通過實證探究了最優掛單策略的分佈規律。圖5表示的是買賣價差狀態為1時最優買單掛單的位置分佈,可以發現當空頭庫存較高時,最優策略傾向於在次優買價掛單,以減少庫存風險;隨著空頭庫存單的減少或者多頭庫存的上升,由於庫存偏向性不高,最優策略偏向於在買一價掛單;當多頭庫存上升到一定程度時,庫存風險較高,做市商停止掛單。

img

圖5:2017 年 8 月 14 日買賣價差狀態為 1 時,最優買單位置(來源:華泰期貨研究院)

註:圖中紅色表示在次優價掛單,黃色表示在最優價掛單,白色表示停止掛單。

通過在滬銅期貨日盤上的回測研究發現,GP模型的做市收益十分穩定,但是做市策略的盈利大概是手續費的10%,並且與手續費高度相關(圖6)。因此手續費的返還水平需要達到90%以上才能實現盈利,也就是說GP模型的整體收益十分依賴於手續費的返還水平。同時,高成交量導致的高手續費與高撤單量也是GP模型的一大缺陷,由於GP模型採取在買一賣一或者更差的價位上報價的方式,多數情況限價單成交都只有跳價的一半,導致每次成交產生的利潤都十分有限。

從整體來看,GP模型的優勢在於充分考慮了庫存風險的管理,並且考慮了市場上報價的跳價現象,在離散報價的基礎上進行最優策略的研究,但從實證結果來看,GP模型的策略收益很大程度上依賴於手續費返還的比例,並且對手續費返還的要求較高。

img

圖6:GP 模型做市策略收益(來源:華泰期貨研究院)


參考文獻

[1] Demsetz H. The cost of transacting[J]. The quarterly journal of economics, 1968, 82(1): 33-53.

[2] Chakraborty T, Kearns M. Market making and mean reversion[C]//Proceedings of the 12th ACM conference on Electronic commerce. 2011: 307-314.

[3] Smidt S. Which road to an efficient stock market: free competition or regulated monopoly?[J]. Financial Analysts Journal, 1971, 27(5): 18-20.

[4] Garman M B. Market microstructure[J]. Journal of financial Economics, 1976, 3(3): 257-275.

[5] Ho T S Y, Stoll H R. The dynamics of dealer markets under competition[J]. The Journal of finance, 1983, 38(4): 1053-1074.

[6] Stoll H R. The pricing of security dealer services: An empirical study of NASDAQ stocks[J]. The journal of finance, 1978, 33(4): 1153-1172.

[7] Ho T, Stoll H R. Optimal dealer pricing under transactions and return uncertainty[J]. Journal of Financial economics, 1981, 9(1): 47-73.

[8] Madhavan A, Smidt S. A Bayesian model of intraday specialist pricing[J]. Journal of Financial Economics, 1991, 30(1): 99-134.

[9] Bagehot W. The only game in town[J]. Financial Analysts Journal, 1971, 27(2): 12-14.

[10] 加密資產衍生品新藍海,期權交易詳解,2020.https://www.odaily.com/post/5146681

[11] Avellaneda M, Stoikov S. High-frequency trading in a limit order book[J]. Quantitative Finance, 2008, 8(3): 217-224.

[12] Guilbaud F, Pham H. Optimal high-frequency trading with limit and market orders[J]. Quantitative Finance, 2013, 13(1): 79-94.

[13] 華泰期貨-股指期貨高頻做市策略的政策性影響-190425

[14] 華泰期貨-股指期貨高頻做市策略的庫存風險管理-190520

[15] 華泰期貨-量化專題報告:基於離散報價的高頻做市策略-190730

市場有風險,投資需謹慎。以上陳述僅作為對於歷史事件的回顧,不代表對未來的觀點,同時不作為任何投資建議。

期貨報價訂閱

import shioaji as sj
import datetime as dt
import pandas as pd
import signal
import os
import sys
import json
import time

# from datetime import timedelta
from dateutil import relativedelta
from line_notify import LineNotify
from datetime import timezone, timedelta


tz = timezone(timedelta(hours=+8))


class Watcher:
    def __init__(self):
        self.child = os.fork()
        if self.child == 0:
            return
        else:
            self.watch()

    def watch(self):
        try:
            os.wait()
        except KeyboardInterrupt:
            self.kill()
        sys.exit()

    def kill(self):
        try:
            print("kill")
            os.kill(self.child, signal.SIGKILL)
        except OSError:
            pass


def get_week_of_month(day_of_month=dt.datetime.now(tz).day):
    return (day_of_month - 1) // 7 + 1


def getFutureDate(today=dt.datetime.now(tz)):
    week_of_month = get_week_of_month()
    weekday = today.weekday()
    print(today, week_of_month, weekday)
    # 第三週 星期三 小於 15:00 周選就是月選
    if (
        week_of_month == 3
        and weekday == 2
        and today < dt.datetime(today.year, today.month, today.day, 15, 0)
    ):
        return str(today.year) + str(today.month + 1).zfill(2)
    else:
        return str(today.year) + str(today.month).zfill(2)


def getOpenPrice(api, year, month, day, open_time):
    FutureDate = getFutureDate()
    TXF = "TXF" + FutureDate
    date = dt.datetime(year, month, day).strftime("%Y-%m-%d")
    kbars = api.kbars(api.Contracts.Futures.TXF[TXF], date)
    df = pd.DataFrame({**kbars})
    df.ts = pd.to_datetime(df.ts)
    df.set_index("ts", inplace=True)
    open_price = df.iloc[df.index.get_loc(open_time, method="nearest")]["Open"]
    return open_price


def getOptionsDealts(api, OP, year, month, day):
    date = dt.datetime(year, month, day).strftime("%Y-%m-%d")
    try:
        # print(OP[:3], OP, date)
        ticks = api.ticks(api.Contracts.Options[OP[:3]][OP], date)
        df = pd.DataFrame({**ticks})
        if df.empty:
            return pd.DataFrame()
        df.ts = pd.to_datetime(df.ts)
        df["OP"] = OP
    except:
        # print(OP)
        return pd.DataFrame()
    return df


# {1: buy deal, 2: sell deal, 0: can't judge}
def set_tick_type(df):
    if df["close"] == df["bid_price"]:
        return 1
    elif df["close"] == df["ask_price"]:
        return 2
    else:
        return 0


@sj.on_quote
def quote_callback(topic: str, quote: dict):
    TAG = str(topic.split("/")[0])
    # print(topic, TAG, json.dumps(quote, indent=4, ensure_ascii=False))
    print(TAG, quote)
    # if TAG == 'L':
    #    #print(f'逐筆報價:{quote}')
    #    print(topic)
    #    print(f"Time:{quote['Date']} {quote['Time']}, Close:{quote['Close']}", len(TICKS))
    #    TICKS = TICKS.append({'time':f"{quote['Date']} {quote['Time']}",
    #                                  'price':quote['Close'][0]}, ignore_index=True)
    # elif TAG == 'Q':
    #    pass
    #    #print(f"AskPrice:{quote['AskPrice']}, BidPrice:{quote['BidPrice']}")
    #    #print(f'五檔報價:{quote}')
    # elif TAG == 'I':
    #    print(topic)
    #    print(f"代碼:{quote['Code']}", \
    #          f"日期:{quote['Date']} {quote['Time']}\n", \
    #          f"開盤價:{quote['Open']}", \
    #          f"最低價:{quote['Low']}", \
    #          f"最高價:{quote['High']}", \
    #          f"成交價:{quote['Close']}", \
    #          f"總金額:{quote['AmountSum']}", \
    #          f"總成交張數:{quote['VolSum']}", \
    #          f"總成交筆數:{quote['Cnt']}",
    #          f"漲跌價:{quote['DiffPrice']}", \
    #          f"漲跌幅:{quote['DiffRate']}")
    # elif TAG == 'MKT':
    #    Code = str(topic.split('/')[-1])
    #    # TickType:1代表外盤、2代表內盤
    #    #print(quote['TickType'][0], type(quote['TickType'][0]))
    #    print(f"代碼:{Code}", \
    #          f"日期:{quote['Date']} {quote['Time']}\n", \
    #          f"成交價:{quote['Close'][0]}", \
    #          f"總金額:{quote['AmountSum'][0]}", \
    #          f"成交盤:{'外盤成交' if quote['TickType'][0] == 1 else '內盤成交'}", \
    #          f"總成交張數:{quote['VolSum'][0]}")


def main():
    api = sj.Shioaji(simulation=False)
    with open(os.environ["HOME"] + "/.mybin/login.txt", "r") as f:
        kw_login = json.loads(f.read())
        accounts = api.login(**kw_login, contracts_timeout=300000)

    FutureDate = getFutureDate()
    MXF = "MXF" + FutureDate
    TXF = "TXF" + FutureDate
    # print(api.Contracts.Futures.MXF)
    api.quote.subscribe(
        api.Contracts.Futures.TXF[TXF],  # 期貨Contract
        quote_type=sj.constant.QuoteType.Tick,  # 報價類型為Tick
        # version=sj.constant.QuoteVersion.v1,  # 回傳資訊版本為v1
    )
    api.quote.set_callback(quote_callback)
    print(MXF, TXF)
    while True:
        time.sleep(1)


if __name__ == "__main__":
    Watcher()
    main()


選擇權訂閱

import shioaji as sj
import datetime as dt
import pandas as pd
import signal
import os
import sys
import json
import time

# from datetime import timedelta
from dateutil import relativedelta
from line_notify import LineNotify
from datetime import timezone, timedelta


tz = timezone(timedelta(hours=+8))


class Watcher:
    def __init__(self):
        self.child = os.fork()
        if self.child == 0:
            return
        else:
            self.watch()

    def watch(self):
        try:
            os.wait()
        except KeyboardInterrupt:
            self.kill()
        sys.exit()

    def kill(self):
        try:
            print("kill")
            os.kill(self.child, signal.SIGKILL)
        except OSError:
            pass


def get_week_of_month(year, month, day):
    """
    獲取指定的某天是某個月的第幾周
    週一為一週的開始
    實現思路:就是計算當天在本年的第y周,本月一1號在本年的第x周,然後求差即可。
    因為查閱python的系統庫可以得知:
    """
    begin = int(dt.date(year, month, 1).strftime("%W"))
    end = int(dt.date(year, month, day).strftime("%W"))
    return end - begin + 1


def getFutureDate(today=dt.datetime.now(tz)):
    week_of_month = get_week_of_month(today.year, today.month, today.day)
    weekday = today.weekday()
    # 第三週 星期三 小於 15:00 周選就是月選
    if (
        week_of_month == 3
        and weekday == 2
        and today < dt.datetime(today.year, today.month, today.day, 15, 0)
    ):
        print(today.year, today.month)
        return str(today.year) + str(today.month).zfill(2)
    else:
        return str(today.year) + str(today.month + 1).zfill(2)


def getOpenPrice(api, year, month, day, open_time):
    FutureDate = getFutureDate()
    TXF = "TXF" + FutureDate
    date = dt.datetime(year, month, day).strftime("%Y-%m-%d")
    kbars = api.kbars(api.Contracts.Futures.TXF[TXF], date)
    df = pd.DataFrame({**kbars})
    df.ts = pd.to_datetime(df.ts)
    df.set_index("ts", inplace=True)
    open_price = df.iloc[df.index.get_loc(open_time, method="nearest")]["Open"]
    return open_price


def get_week_of_month(year, month, day):
    """
    獲取指定的某天是某個月的第幾周
    週一為一週的開始
    實現思路:就是計算當天在本年的第y周,本月一1號在本年的第x周,然後求差即可。
    因為查閱python的系統庫可以得知:

    """

    begin = int(dt.date(year, month, 1).strftime("%W"))
    end = int(dt.date(year, month, day).strftime("%W"))

    return end - begin + 1


def getOptionsDealts(api, OP, year, month, day):
    date = dt.datetime(year, month, day).strftime("%Y-%m-%d")
    try:
        # print(OP[:3], OP, date)
        ticks = api.ticks(api.Contracts.Options[OP[:3]][OP], date)
        df = pd.DataFrame({**ticks})
        if df.empty:
            return pd.DataFrame()
        df.ts = pd.to_datetime(df.ts)
        df["OP"] = OP
    except:
        # print(OP)
        return pd.DataFrame()
    return df


# {1: buy deal, 2: sell deal, 0: can't judge}
def set_tick_type(df):
    if df["close"] == df["bid_price"]:
        return 1
    elif df["close"] == df["ask_price"]:
        return 2
    else:
        return 0


def main():
    api = sj.Shioaji(simulation=False)
    with open(os.environ["HOME"] + "/.mybin/login.txt", "r") as f:
        kw_login = json.loads(f.read())
        accounts = api.login(**kw_login, contracts_timeout=300000)

    now = dt.datetime.now(tz)
    wm = get_week_of_month(now.year, now.month, now.day)
    n_w = dt.datetime.today().weekday()
    # print(wm, n_w)
    # print(api.Contracts.Options)
    # print(now.strftime("%Y%m"))
    week_delivery_month = now.strftime("%Y%m")
    month_delivery_month = (
        (now + relativedelta.relativedelta(months=1)).strftime("%Y%m")
        if wm >= 3 and n_w >= 2
        else now.strftime("%Y%m")
    )
    print(week_delivery_month, month_delivery_month)
    month_options = []
    week_options = []
    strike_price_list = []
    for option in api.Contracts.Options:
        for o in option:
            if "TX" in o["category"] and (
                o["delivery_month"] == week_delivery_month
                or o["delivery_month"] == month_delivery_month
            ):
                strike_price_list.append(int(o["strike_price"]))
                if o["category"] == "TXO":
                    month_options.append(o["symbol"])
                else:
                    week_options.append(o["symbol"])
    now = dt.datetime.now(tz)
    week_of_month = get_week_of_month(now.year, now.month, now.day)
    weekday = now.weekday()

    # 第三週 星期三 小於 15:00 周選就是月選
    if (
        week_of_month == 3
        and weekday == 2
        and now < dt.datetime(now.year, now.month, now.day, 15, 0)
    ):
        week_options = month_options

    EndDate = dt.datetime.now(tz) + dt.timedelta(days=1)
    StartDate = EndDate - dt.timedelta(days=7)
    DataFrameList = []
    OptionsDict = {}

    if now.hour < 15:
        open_time = dt.datetime(now.year, now.month, now.day, 8, 45, 0)
    else:
        # open_time = dt.datetime(now.year, now.month, now.day, 8, 45, 0)
        open_time = dt.datetime(now.year, now.month, now.day, 15, 0, 0)

    open_price = getOpenPrice(api, now.year, now.month, now.day, open_time)
    strike_price_list = sorted(list(set(strike_price_list)))
    strike_price = min(strike_price_list, key=lambda x: abs(x - open_price))
    # print(strike_price_list, strike_price)
    # print(week_options)
    week_options = [s for s in week_options if str(strike_price) in s]
    print(week_options)

    while StartDate <= EndDate:
        for OP in week_options:
            df = getOptionsDealts(
                api, OP, StartDate.year, StartDate.month, StartDate.day
            )
            if not df.empty:
                columns = [
                    "OP",
                    "ts",
                    "close",
                    "volume",
                    "ask_price",
                    "ask_volume",
                    "bid_price",
                    "bid_volume",
                ]
                df = df[columns]
                df = df.assign(tick_type=df.apply(set_tick_type, axis=1))
                DataFrameList.append(df)
        StartDate = StartDate + dt.timedelta(days=1)

    if DataFrameList != []:
        df_final = pd.concat(DataFrameList, axis=0, ignore_index=True)
        # print(df_final.to_markdown())
        options = list(set(df_final["OP"].tolist()))
        # print(options)
        for option in options:
            indexs = df_final[df_final.OP == option].index.tolist()
            # print(indexs)
            # print(df_final.iloc[indexs])
            OptionsDict[option] = (
                df_final.iloc[indexs].sort_index(ascending=True).reset_index(drop=True)
            )

    Resistance_Support = {}
    for key, value in OptionsDict.items():
        # print(value, type(value))
        value.ts = pd.to_datetime(value.ts)
        value.set_index("ts", inplace=True)
        for t, row in value.iterrows():
            if t >= open_time:
                if key[-1] == "P":
                    print(open_price + row.close)
                    Resistance_Support["Resistance"] = open_price + row.close
                else:
                    print(open_price - row.close)
                    Resistance_Support["Support"] = open_price - row.close
                break

    LineNotify("KXwzqEGtIp1JEkS5GjqXqRAT0D4BdQQvCNcqOa7ySfz").send(
        f"\nOpen Price: {open_price} \nResistance: {Resistance_Support['Resistance']} \nSupport: {Resistance_Support['Support']}"
    )
    LineNotify("eTd1BOTLRXu4VtFWNwwFr3DjOcQnm7ZdLIyEuR4ZgII").send(
        f"\nOpen Price: {open_price} \nResistance: {Resistance_Support['Resistance']} \nSupport: {Resistance_Support['Support']}"
    )
    print("OPEN PRICE:", open_price, open_time)
    # open_price = df.iloc[df.index.get_loc(open_time, method="nearest")]["Open"]


if __name__ == "__main__":
    Watcher()
    print(getFutureDate())
    input()
    main()

三角套利策略

策略說明

三角套利又叫間接套利或多邊套利,起源於外匯市場中利用交叉匯率定價錯誤進行的套利。

所謂三角套利,是一種引入三種貨幣的套利手段。它利用三種外匯對合理交叉匯率的暫時性偏離來實現套利。理論上,如果我們擁有很低延遲的下單平臺,並且可以獲得較低的買賣價差,那麼我們有機會實現無風險套利

三角套利在數字貨幣市場同樣適用,通常情況下,數字貨幣之間的匯率與其相對應的美元價格相關。但由於數字貨幣市場波動性較強,部分交易所由於流動性不足等各種原因,會造成某些時刻,合成交叉價格和市場價格的暫時偏離,當這種偏離足夠抵消我們的交易成本時,我們便可使用三角套利方法實現無風險利潤。

適用情況:行情有較大波動時,不同交易標的漲跌幅不同導致的不同交易對之間的價格變動之後。

舉個例子,現貨市場現有這樣三個交易對:BTC/USDT,ETH/USDT,ETH/BTC。

假設前提是:市場中的手續費為零:

BTC/USDT買1 = 9999 USDT,賣1=10000 USDT;

ETH/USDT買1 = 299 USDT,賣1 = 300 USDT;

ETH/BTC買1 = 0.029901 BTC,賣1 = BTC 0.03001 BTC。

我們根據BTC/USDT以及ETH/USDT的現價計算出ETH/BTC的現價,計算後得知ETH/BTC的理論買1 = 0.029902 BTC,賣1 = 0.03000 BTC,與市場現價基本吻合。

若某時刻價格出現波動,ETH/BTC買1變為0.038 BTC,賣1變為0.039 BTC,另外兩個交易對價格不變

最終,所有盈虧均反映在USDT上,我們在市場行情的波動下,利用價差實現了三角套利。

優勢:受交易標的價格漲跌影響較小,無行情劇烈變動導致的大額虧損,總體風險較小。

劣勢:掛單變動導致價格滑點;交易存在手續費成本;可能存在套利後未及時兌換成穩定幣,持有幣種價格下跌導致虧損的風險;受交易資料延遲以及交易所訂單撮合性能影響較大。

此外,KuCoin擁有level3等級的交易資料、極優的撮合引擎,以及對api使用者提供特別的手續費折扣,極大程度的減少了你在策略實施時的劣勢,同時提供sandbox環境作為資料測試支撐,幫助你規避風險。

請注意,任何策略在使用時需要做好風險管理,如果你想在實際環境中利用策略獲得穩定的盈利,我們希望你能夠在sandbox環境配合其他參數或是策略進行測試調整,以使你能夠達到目的,我們也非常期待你能分享你的測試資料以及獨到的見解。

當然,如果這個過程中,你遇到任何問題需要幫助亦或是有賺錢的策略想要分享,請在ISSUE中反映,我們會努力及時響應。

如果你對該策略有興趣,請點選右上角star,我們會根據star個數來衡量策略的受歡迎程度和後續最佳化優先順序,你也可以點選右上角watching通過接收更新通知來持續關注該項目

比特幣怎麼低風險套利?不知道這6點不要買比特幣(建議收藏)

先說結論:

1.比特幣套利不是騙局,但是,比特幣套利機會很少。

2.收費教你比特幣搬磚套利的100%是騙子,別問我為什麼知道。

3.拿不準時,記得翻翻我這篇文章(文章略長,可以先收藏)。

OK,下面我們進入正題,比特幣套利方法有很多種,一般人我不告訴他。常見的有現貨搬磚、期現對沖、期期對沖、跨平臺合約對沖、跨品種套利、大週期套利(適合小白)。


(1)現貨搬磚

什麼是現貨交易?一手交錢,一手交貨。比如你去菜市場買菜,這就是一種現貨交易。

由於比特幣在不同交易所存在價差,這就產生了搬磚套利的概念。就好像同樣的土豆超市1斤賣5塊,而菜市場1斤才2塊,於是你可以從菜市場買土豆,賣給超市,這便是套利原理。具體到比特幣來說,現貨搬磚的方法有兩個:

  • 在A平臺買入比特幣,在B平臺賣出。好處是風險低,缺點是手續費高,搬磚一次十分麻煩,利潤低。
  • 在A和B兩個平臺同時存入比特幣和USDT,在價格低的平臺買入比特幣,價格高的平臺賣出比特幣。這種方法的缺點是資金佔用大,有時需要承擔一定的波動風險。

(2)期現對沖

期現對沖也是一種常見的比特幣套利方法,這種方法的好處是風險低,效率高,我見過牛逼的策略年化超過100%,回撤才百分之幾,缺點是門檻略高,一般人玩不了。

由於比特幣期貨和現貨之間常常存在價差,因此產生了套利機會。什麼是比特幣期貨?大家可以把期貨理解成一種可以賣出的品種,舉例,假如幣小寶判斷未來比特幣價格下跌,即使我手裡沒有比特幣,也可以賣出,這便是期貨交易。(要是不理解期貨相關概念,建議先收藏文章)

  • 期現對沖的基本方法是:在某一價位買入比特幣,同時在比特幣期貨賣出比特幣。當比特幣期貨價格低於現貨價格時,平倉期貨,同時賣出比特幣;當比特幣現貨價格高於期貨時,賣出現貨,平掉期貨。
  • 舉例,幣小寶在6萬塊的位置買入了1枚比特幣現貨,同時在該價位賣出1枚比特幣期貨。10天以後,比特幣現貨跌至5.2萬,而比特幣期貨卻跌至5萬。賣出現貨,幣小寶虧損0.8萬;平倉期貨,幣小寶賺了1萬。一來一去,賺了0.2萬。

(3)期期對沖(交割合約)

期期對沖是利用不同期貨品種進行套利。比特幣期貨分為永續合約和交割合約。交割合約又分本週,次周,季度,次季度。由於不同合約間差價不同,而短期合約都會涉及到交割從而產生了套利機會。

  • 比如比特幣本週合約5萬塊,指數5.2萬,季度5.5萬。即可做多本週,等比例做空季度。等交割時平倉獲利即可。

期期對沖需要非常專業的水平,建議慎重參與。

(4)跨平臺合約對沖

這種方法是針對永續合約的。幣小寶參與過一段時間,個人覺得風險收益比較低。

與現貨類似,不同平臺之間的永續合約也存在一定價差。

  • 可以在價格低的平臺做多比特幣永續,價格高的平臺做空比特幣永續,待價格回歸後平倉即可。

(5)跨品種套利

跨品種套利主要是指比特幣不同交易對之間的套利,有時也稱三角套利。比如BTC/USDT、BTC/ETH、ETH/USDT這三個交易對之間由於行情波動存在套利機會。

  • 舉例,BTC/USDT=10000;BTC/ETH=50;ETH/USDT=220
  • 此時,買入BTC,花費10000美金,之後將BTC全部換成50ETH,最後再將ETH賣出50*220=11000。這筆交易獲利1000美金。

不過,實際交易中這種機會比較少,需要碰運氣,這種方法的優勢是風險非常低。

(6)大週期套利(適合小白)

很多人看了上面的幾點,還是不明白怎麼進行比特幣套利。下面講一種非常簡單的套利方法,也就是大週期套利。

這個方法是幣小寶獨家首創哈。先說這個方法的缺點:持有週期長(3~6個月或更長),交易機會少(1年1次),好處是操作簡單,風險可控,適合小白。

  • 大週期套利原理是:比特幣價格一般會圍繞長期開採成本大幅波動。當比特幣價格大幅低於開採成本時,果斷買進,高於開採成本時,賣出。
  • 舉例:比特幣在2018年12月至2019年3月之間價格小於3萬,而當時最好的礦機挖一枚比特幣都得4萬多,因此,可以果斷買入。而比特幣歷史價格也顯示,在這一區域買入獲利的概率非常高。

unnamed.png

而今年的3月份市場大跌,比特幣一度跌到3.8萬,再次出現了一次非常強烈了買入機會。如果你不知道怎麼看挖礦成本,還有一個辦法是,根據比特幣歷史K線判斷。

  • 當比特幣價格低於周線MA256時,選擇買入,高於MA256時,賣出。


總結,目前市場確實存在很多比特幣套利策略,但是萬變不離其宗,套利的本質是價差。與一般的炒幣相比,套利風險低,吸引了大量的專業玩家介入。目前市場一般都是程序化套利,類似於股票行業的高頻交易。因此,對於一般人門檻較高。比特幣套利確實也不乏大牛,一年幾倍時有發生,但那都是常人難以企及的高度。幣小寶認為,大週期套利雖然交易機會少,但是好在門檻低。

總之,要想對比特幣套利深入瞭解,還需要在實踐中進行提升,切勿一知半解盲目投資,否則可能遇到各種名義的搬磚套利騙子(比如至今仍然存在的火幣搬磚套利騙局)。本文主要拋磚引玉,期待大家在比特幣投資方面取得更大的成就。

配對交易

本益比配對

「多頭市場容易炒高本益比,到了空頭市場就會現出原形。因此,買進低本益比個股,再同時放空同等金額的高本益比個股,不論市場大漲大跌,都有賺錢機會。」鄭廳宜舉例,像東洋(4105)今年每股盈餘(EPS)有機會挑戰7元以上,本益比約10倍。反觀,第一銅(2009)今年第一季EPS僅為0.02元左右,本益比30倍以上。

以目前東洋約67元,第一銅約32元配對,就可買進一張東洋,同時放空2張第一銅。同樣的道理,鄭廳宜的多空配對交易,還包括做多目前本益比不到9倍的明安(8938),同時放空一張本益比約90倍的怡利電(2497)。「就像科斯托蘭尼提出的主人與狗理論,股價終究會回歸基本面,利用這種多空配對方式度過空頭市場,心才不會慌。」

股市多空不明時的投資技巧 – 配對交易(或稱價差交易)解析

https://usstockinvesting.com/what-is-pairtrade/

2022年投資很難,因為S&P500的走勢紛亂。要跌跌不了太久、要漲又只漲一下下。如果你跟我一樣習慣順著趨勢做,那今年真的不容易。這時候,可以參考「配對交易(Pair Trade)」(或稱價差交易)。因為大環境雖然趨勢不明顯,但產業中仍有一些趨勢。配對交易藉由一多一空而抵銷掉大環境的紛亂,凸顯出產業中的差異。

內容目錄2022年大盤雖然紛亂,但還是有「價差趨勢」配對交易 – 把資金分成兩半,一半買進、一半放空配對交易第一種 – 價差收斂型 – 利用價差縮小而獲利收斂型配對交易 – 慎防價差突然變化!配對交易第二種 – 發散型 – 利用價差擴大賺錢發散型配對交易的好處 – (理想情況下)股市大漲大跌都能賺錢如何畫出股價比例圖 – 用Stockcharts配對交易組合參考 – 作多那斯達克證交所集團,放空那斯達克100指數ETF的配對交易組合參考 – 作多標準普爾500,放空那斯達克100指數結論:股市多空不明時,可以掌握兩組價差逐漸擴大的股票組合,順著趨勢交易

2022年大盤雖然紛亂,但還是有「價差趨勢」

2022年的大盤走勢紛亂,下圖是S&P500今年的走勢圖:

img

雖然現在回過頭看好像是越走越低,似乎有個趨勢,但之前可沒那麼明顯(也可能是我功力還不夠😆)如果你跟我一樣,那今年就很辛苦了,買進也不是、放空也不是,動不動就觸發停損。

但今年也有像下圖這樣的組合,五月後價格一路向上,趨勢非常明顯,很適合我們這樣習慣順著趨勢操作的人:

img

或許有人問:「這是哪檔股票?」,或者有人想:「經濟這麼亂的時局這種股票很危險啦,不知道什麼時候會掉下來。」

但還好,它不是股票,而是兩檔鋼鐵公司相除的比率,分別是Steel Dynamic(股票代號:STLD)和克里夫蘭鋼鐵(股票代號:CLF),我們可以「買進STLD、放空CLF」來獲利。這就是「配對交易」。

2022年12月23日更新:這檔組合已因克里夫蘭鋼鐵談到大單而改變趨勢、跌破季線停損

配對交易 – 把資金分成兩半,一半買進、一半放空

配對交易又有人稱作價差交易,英文是Pair Trade,也有人稱「L / S策略(Long / short)」,不管名稱如何,概念都是「把資金分成兩半,一半做多,一半放空」。

「一半做多、一半放空」可以抵銷掉大環境的風險,只留下兩個不同標的之間的價差,然後利用價差的變化來賺錢。也因為配對交易不論大環境好壞都能賺錢,所以屬於絕對報酬型策略。

然而,配對交易並不是毫無缺點,它最大的缺點就是「賺得較少」

在股市一直漲、套牢也只要幾週就能獲利的大好時期,配對交易不受青睞。因為配對交易一半資金一定要做空,所以賺得絕對沒有單純買進來得多。只是,在2022年這種多空不明、股市紛亂的時期,配對交易就有用了。

但要做多什麼?放空什麼?這就因不同的哲學而異了。配對交易有兩種類型…

配對交易第一種 – 價差收斂型 – 利用價差縮小而獲利

這是許多避險基金在用的方式,他們尋遍市場,找到兩個價差穩定的股票或者標的,比方說布蘭特原油和西德州原油。理論上,布蘭特原油和西德州原油價差變化不大,畢竟都是石油嘛,價格應該會有個固定的範圍。

下圖是「西德州原油/布蘭特原油」的線圖,可以看到,這兩種油價的比例變動不大:

img

所以,當布蘭特原油跟西德州原油價差拉大的時候,我們就能賭它的價差會縮小,就可以放空上漲的標的、作多下跌的標的,像下圖這樣:

img資料來源:百舜整理

只要價差最終真的會回到固定的比例,這樣就能穩定獲利。但是,世界上沒那麼好的事…

收斂型配對交易 – 慎防價差突然變化!

收斂型配對交易有個問題,你得緊跟這個產業、掌握這兩個標的的價差才行,免得有些事情發生了不知道,還傻傻以為未來會像過去一樣,都會回到以前的比例。結果卻越差越遠、多空都賠。

下圖一樣是西德州原油和布蘭特原油的價格比例,但時間是2008~2015年:

img

可以看到,當時的價差沒什麼固定範圍,十分紊亂。

因為那時候頁巖油崛起,新開採技術改變了很多事情,價差就一直變動、不再是固定比率了。這時候如果還一直想著「反正最終會回到固定範圍」,然後去放空或者作多,那就是兩邊都賠,甚至可能一次賠掉好幾年的利潤。

所以,如果你不是專業的交易員,我實在不建議你用「收斂型的配對交易」。比較適合我們的是這一種:

配對交易第二種 – 發散型 – 利用價差擴大賺錢

這是我今天想討論的主題,也是文章開頭那兩檔鋼鐵股組合「STLD / CLF」大漲的原因,這也最接近「順勢交易」的哲學 – 讓趨勢向前奔跑。只要趨勢還在,我們抱著這個組合即可賺錢。

為什麼兩間公司的價差會逐漸擴大呢?這要從股價的組成說起。會影響股價的因素很多,但可以概分為兩大類:「大環境」跟「公司營運」。所有同產業的公司都面臨相同的大環境,比方說疫情時各國封鎖國境,所有旅行社都很慘;或者像2021年上半年供應鏈吃緊、鋼價大漲時,所有鋼鐵廠都受惠。

只是相同的大環境下,有的公司就是比較強壯,有的就是比較慘一點,這就取決於各公司的營運狀況了。而這個時候,他們的股價雖然都下跌,但可能A公司跌10%、B公司跌20%;而當大盤大漲的時候,可能變成A公司漲15%、B公司漲10%。

大環境不會一夕之間轉變,公司營運狀況也不會突然改變。因此隨著時間過去,價差可能會越拉越開,就會像「STLD/CLF」的組合一樣,有明顯的趨勢。

(想詳細瞭解為什麼STLD / CLF會上漲的話,可以參考這篇文章:<美股分析> 2022年至今還漲70%左右的美國 鋼鐵 股票是誰?未來鋼鐵 股票 還有機會嗎?我認為有些疑慮,因為這個原因….

發散型配對交易的好處 – (理想情況下)股市大漲大跌都能賺錢

當大盤趨勢不明顯時,買進容易停損,放空也很容易被反彈給弄到停損,因此很容易兩邊都賠錢,吃力不討好。

但如果是用配對交易,理想情況下,當大盤上漲時,作多那一半會賺錢,放空那一半會賠錢,而作多那一半的獲利,會超過放空那一半的虧損;當大盤下跌時,作多的那一半會賠錢,放空的那一半會賺錢,此時,做空那一半的獲利,會超過作多那一半的虧損

所以,大盤上漲或下跌,發散型的配對交易都可能賺錢。

只可惜,這種好事無法長久,就跟再會漲的股票總會漲完一樣。很可能價差擴大的趨勢突然變了,本來很弱的公司扭轉頹勢、沒那麼弱;本來很強的公司突然碰上麻煩、變弱了。任何情況都有可能。

但就像傳統的順勢交易也要跌破某個條件時停損一樣(比方說跌破季線停損之類的),當兩間公司的價差趨勢改變時,同時賣出作多和做空部位,出場就好,一樣可以「截斷虧損,讓利潤向前奔跑。

如何畫出股價比例圖 – 用Stockcharts

那麼,要怎麼畫出兩個標的的價差線圖呢?用Stockcharts就可以了。

(延伸閱讀:免費、有各種技術指標、能同時瀏覽多檔線圖的網站 – 「StockCharts」!(內附技術指標名詞中英對照)

只要在平常輸入股票代碼的地方,輸入想配對的兩組股票代碼,然後中間用冒號隔開:

img

這樣就會出現線圖了:

img

此時就能夠加上你熟悉的指標、用你習慣的方式進出場了。

配對交易組合參考 – 作多那斯達克證交所集團,放空那斯達克100指數

配對交易非常靈活,除了個股之外,也可以用「強勢股vs. 大盤指數」來配對。如果某個公司就是比大盤強勢,那可以考慮「作多強勢股,放空大盤ETF」的配對組合,像是「作多那斯達克證交所集團,放空那斯達克100指數」。

那斯達克100指數(ETF股票代號:QQQ)2022年慘兮兮,因為很多半導體公司2022年都說庫存過多,3C賣得不好;其他科技股也因為升息關係跌得很凶,但那斯達克證交所集團(股票代號:NDAQ)卻好多了。

因為,那斯達克證交所不是靠賣晶片和賣軟體賺錢,而是靠成交量和大數據賺錢。當股票忽漲忽跌時,表示波動很大,對做短線的人來說反而是好事,所以成交量不會跟著掉(但是當經濟衰退、資金離開股市時成交量也會大幅下滑,此時證交所股票一樣很慘,千萬要留意)。

(延伸閱讀:美國股市指數中的四大指數有兩個是它的 – 那斯達克證券交易所集團

下圖是NDAQ / QQQ的線圖,可以看到,NDAQ/QQQ在2022年下半年一路往上漲:

img資料來源:Stockchart

此時我們就可以用以往股市大漲時期的習慣方式來交易它,然後等到趨勢反轉時停損。如果你有心目中的強勢股,也可以拿來和放空大盤的ETF作配對交易組合。

ETF的配對交易組合參考 – 作多標準普爾500,放空那斯達克100指數

除了個股之外,ETF也可以配對交易。像2022年科技股因為半導體庫存增加和解封等關係而大跌,科技股就比其他類股弱勢許多。2022年SPY / QQQ的線圖就像下圖這樣,一路往上漲:

img

此時,可以考慮把資金分兩半,一半作多標準普爾500指數ETF(股票代號:SPY),一半放空那斯達克100指數ETF(股票代號:QQQ)(或者直接買一倍放空QQQ的ETF,股票代號:PSQ),等到SPY / QQQ的走勢轉變時停損。

至於為什麼SPY / QQQ會一路上漲呢?是因為指數計算方式的關係,科技股佔QQQ的權重比SPY大多了,所以科技股弱勢時,對QQQ的影響比SPY大。想詳細瞭解,可以參考這篇文章:「美國 股市 指數 懶人包,一文解析及比較道瓊工業指數、S&P500指數、那斯達克指數」。

結論:股市多空不明時,可以掌握兩組價差逐漸擴大的股票組合,順著趨勢交易

大盤(像美股就是S&P500)多空不明、趨勢不明顯,但產業中卻仍有明顯的趨勢。所以可以挑選價差逐漸擴大的兩檔股票,作多強勢股、放空弱勢股,然後搭配習慣的交易技巧進出場(像我的話就是跌破季線賣出、或者跌破前波低點賣出)。

至於利用價差收斂賺錢的配對交易,就留給專業的交易員們吧!那個要時時刻刻注意產業變化才行,如果平常還有其他工作,很可能會多空雙賠,吃力不討好。


【觀念分享】

交易策略有很多類型,除了上次介紹的單邊方向性策略之外,還有配對交易、套利交易、資產配置等等。 從廣義上來說,方向性交易是指做單一方向,像是做多臺指期,需100%承受反向波動的風險;配對交易是指同時做多A商品+做空B商品,像是作多臺指期+做空電子期。這時可能有人會想,既然看多臺指,為何還要多此一舉?從我的觀點來看,主要有幾個理由: 一、避開系統性風險 二、單商品行情不明確,不過商品間強弱勢明確 三、高相關性商品之間偏差值高,有套利空間 以下簡單介紹幾種配對交易策略

  • 兩個商品之間做發散:做多強勢+做空弱勢,相關係數最好在0.6~0.8之間比較適合這種做法。
  • 兩個商品做收斂:做空強勢+做多逆勢。收斂是一種逆勢思維,適合在高度相關性的商品上,像是臺灣期交所的臺指期與新加坡交易所的摩臺指,兩者的標的物極類似,且相關係數長年保持在0.9以上。
  • 套利策略:當商品之間出現價格不合理的狀況,就可以在無風險的狀況下套取利潤。最常見的就是利用選擇權的Black-Scholes Model來做。
  • 上下游產品之間的價差:原油的裂解價差(crack spread)與黃豆的榨壓價差(Crush Spread),由於臺灣較少人做這類型策略,有興趣的可以自行google。 下次將介紹投資組合與資產配置,敬請期待

史上最詳盡的海龜交易法則筆記

出處 : https://zhuanlan.zhihu.com/p/158083885

海龜交易的一切都源於全世界最著名的期貨投機家理查德 丹尼斯和他的好朋友比爾 埃克哈特的一場爭論,就是傑出的交易者究竟是天生的還是培養出來的。丹尼斯相信他幾乎可以把任何一個人變為優秀的交易者,埃克哈特則認為這是一種天賦問題,不是培養問題。丹尼斯願意用自己的錢來證明自己的話,於是兩人打了一個賭。

為此,他們在《華爾街日報》、《巴倫週刊》和《紐約時報》上刊登了大幅廣告,宣佈丹尼斯正在招募培訓生,他會把自己的交易方法傳授給這些人,然後給每個人一個100萬美元的交易帳戶。這些培訓生就被叫做海龜。

我認為海龜交易法則是一套投機理念,它的技術規則、操作規則與心理解析共同支撐了這套交易系統,而不是單單根據一些技術資料進行交易,它的技術資料可能在我們自己的市場得不到驗證,但是整個理念適合於所有的投機市場。

《海龜交易法則》這本書的作者就是丹尼斯的門徒之一,他負責丹尼斯最大的一個帳戶,為丹尼斯賺了3100多萬美元。

海龜的思維

三種類型的交易者

對沖者:一般為企業,通過買賣期貨合約抵消期貨價格波動風險。

帽客:經營市場的流動性風險,希望通過快速與對沖者交易賺取價差。他們為市場創造了流動性,因為他們在不停的出價、要價,寄希望於買單和賣單之間的平衡。

投機者:寄希望短期價格的預判。

img

價格變動取決於市場中所有的買者和賣者的共同態度,這些買賣者就是我們所說的那些帽客、投機者和對沖者:想在一天內反覆賺取微小買賣價差的人(帽客),想投機於一天或幾個星期、幾個月內價格大幅變化的人(投機者),以及想規避經營風險的人。(對沖者)

共同態度變了,價格就會變化。不管什麼原因,一旦賣者不再願意以目前的價格賣出,而是想提高價格,買者又願意接受這個更高的價格時,市場價格就會上漲。反之亦然。

交易過程中出現的情緒:

  1. 希望:我當然希望我買了之後,它馬上就漲
  2. 恐懼:我再也賠不起了,這一次我得躲得遠遠的。
  3. 貪心:我賺翻了,我要把我的頭寸擴大一倍。
  4. 絕望:這個交易系統不管用,我一直在賠錢。

在那些較為簡單原始的環境中,人類已經形成的某些特定的世界觀對他們大有幫助,但在交易世界中,這些認識反而成了障礙。人類認識現實的方式可能出現扭曲,科學家們稱之為認知偏差。以下是幾種對交易行為有影響的認知偏差:

1、損失厭惡:對避免損失有一種強烈的偏好。也就是說,不賠錢遠比賺錢更重要。

患上損失厭惡症的人對避免損失有一種絕對的偏好,盈利只是第二位的。對大多數人來說,沒有賺到100美元與損失了100美元並不是一回事。但是從理性角度來看,這兩者是一回事:它們都代表著這100美元沒有盈利。

2、沉沒成本效應:更重視已經花掉的錢,而不是而來可能要花的錢。

假設ACME公司投入了1億美元開發一種用於生產筆記型電腦螢幕的特殊技術,但在這筆錢已經花掉之後,該公司卻發現另外一種技術明顯更好,而且更有可能及時帶來它所期望的成果。如果從純理性的角度出發,ACME公司應該權衡一下採用這種新技術的未來成本和繼續使用當前技術的未來成本,然後根據未來的收益和支出狀況作出決策,完全不必考慮已經花掉的那些研發投資。

3、處置效應:早早兌現利潤,卻讓損失持續下去。

指投資者傾向於賣掉價格正在上漲的股票,卻保留價值正在下跌的股票。有人認為這種效應與沉沒成本效應有關。

4、結果偏好:只會根據一個決策的結果來判斷它的好壞,而不去考慮決策本身的質量。

5、近期偏好:更重視近期的資料或經驗,忽視早期的資料或經驗。

指一個人更重視近期的資料和經驗。昨天的一筆交易比上個星期或上一年的交易重要。近兩個月的賠錢經歷可能跟過去6個月的賺錢經歷同樣重要,甚至更加重要。於是,近期的一連串不成功的交易會導致交易者懷疑他們的方法和決策程序。

6、錨定效應:過於依賴容易獲得的資訊。他們可能會盯著近期的一個價位,根據當前價格與這個參考價格的關係來作出決策。

7、潮流效應:盲目相信一件事,只因為其它許多人都相信它。

8、小數定律:從太少的資訊中得出沒有依據的結論。

小樣本對總體並沒有太大的代表性。比如,如果一個交易系統在6次測試中有4次成功,大多數人都會說這是一個好系統,但從統計學上說,並沒有足夠多的資訊可以支援這種結論。

一些交易風格

1、趨勢跟蹤

使用這種方法的交易者檢視利用幾個月內的大趨勢。趨勢跟蹤者在市場處於歷史高位或低位的時候入市,如果市場逆轉並維持了幾個星期,他們就會退出。

2、反趨勢交易

與趨勢交易相反,在價格接近新高的時候賣空。他們的理論依據是,新高的突破大多都不會引發市場趨勢。

3、波段交易

本質上與趨勢跟蹤交易相同,只不過它瞄準的是短期市場動向。比如,一次成功的波段交易可能只會持續三四天,而不是幾個月。

4、當日交易

一個真正的當日交易者總是試圖在每天的交易結束之前退出市場。這樣一來,即使夜間爆發的負面新聞引發了開盤之後的劇烈變化,他們的頭寸也不會受到什麼影響。

市場狀態

講技術的書,以下面的描述最為模稜兩可,因為很多參數沒有量化。比如第一個穩定平靜的狀態,較小的範圍是多少就很難界定,而且各人有各人的理解。但是這個問題直接影響到進場的時機,所以又必須進行量化。我不認為所有講述技術的書都是扯淡,也不認為所有講述技術的書都是真實的,最重要的還是得到一種思路,然後通過思路進行實驗,得出一個適合於自己的套路。這個適合於自己包括自己的風險承受能力,自己的資金量和操作手法。為了將下面描述的市場狀態做一個量化,首先假定觀察週期是3個月。

1、穩定平靜

價格在一個相對較小的範圍內上下波動,很少超出這個範圍。

量化指標:如果觀察週期內最高價與最低價的波幅不超過20%,以觀察週期的第一天價格為基準價,週期最後一天價格與基準價格波幅不超過10%,則為穩定平靜狀態。

img

例子:魯抗醫藥2019年9月24日-12月31日

2、穩定波動

有大的日間或周間變化,但沒有重大的月際變化。

量化指標:如果觀察週期內最高價與最低價的波幅在20%-50%之間,週期最後一天價格與基準價格波幅不超過20%,則為穩定波動狀態。

img

例子:堅瑞沃能2020年4月17日-7月24日

3、平靜的趨勢

價格在幾個月中呈現出緩慢的運動或趨向,但始終沒有劇烈的回呼或反方向運動。

量化指標:向上趨勢為將週期分為均等的10段,每一段的波幅都不超過-20%,基準價向下波幅不超過-20%,週期最後一天價格幅度超過基準價60%,向下趨勢正好相反

img

例子:神馳機電2020年1月23日-5月8日

4、波動的趨勢

價格有大的變化,偶爾伴有劇烈的短期逆轉。

量化指標:向上趨勢為基準價向下幅度在-20%以下,週期最後一天價格幅度超過60%,向下趨勢正好相反

img

例子:神馳機電2020年1月20日至4月30日

海龜的課程

1、破產風險

裡面舉了一個擲骰子的例子,一個骰子6個面,假如你擲出4點、5點或6點,你每押1美元就賠給你2美元,那麼如果你口袋裡有1000美元,你每次會押多少錢?是押1000美元、500美元還是100美元?從機率上來說,如果你一次就all in,那麼你有50%的機率破產,如果你每次押500美元,那麼也有25%的機率破產,如果你每次押100美元,那麼你的破產機率是 0.510 =0.097%

破產風險會隨著賭注的增加而不成比例地迅速增大,這是它最重要的特徵之一。如果你把每次的賭金翻一番,破產風險一般不止翻一番,視系統特性的不同,風險有可能翻上兩番、三番甚至四番。

2、資金管理

所謂資金管理,就是指控制市場風險的程度,確保交易者能安然度過每一個交易者都必然要碰到的不利時期。交易者既要讓盈利潛力最大化,又要把破產風險控制在可以接受的水平,資金管理就是這樣一門藝術。

這裡書中的第三章看的不是太明白,所以查閱了知乎上的文章

2.1、首先要計算一個叫做ATR的技術指標,這個技術指標的演算法如下:

2.1.1、當前交易日的最高價與最低價間的波幅

2.1.2、前一交易日收盤價與當前交易日最高價間的波幅

2.1.3、前一交易日收盤價與當前交易日最低價間的波幅

以上三個數取最大值就叫真實波幅,根據迭代演算法,每天都可以取這麼一個真實波幅,比如取14天真實波幅的平均值作為當天的ATR(平均真實波幅),其實就跟移動平均線的演算法是一致的,只不過具體的規則有點區別。

舉個例子:

假設初始資金為init元,讓1個ATR波動表式帳戶的1%,再假設某股票價格為p,當前ATR值為atr,那麼應該買多少股呢?

應該買的股數 = init * 1% / atr

那麼交易的資金 = p * init * 1% / atr= init * 1% * p / atr

如果我們的止損定在1atr,那麼當虧損 = -atr * init * 1% * p / atr = - init * 1% 的時候,就止損

如果我們的止損定在2atr,那麼當虧損 = -2 * atr * init * 1% * p / atr = -2 * init * 1%的時候,就止損。

下面是原版海龜的ATR計算方法

原版海龜ATR的計算方法 真實波幅 = max(H-L, H-PDC, PDC-L) H=當日最高價 L=當日最低價 PDC=前一日收盤價 N = (19 * PDN +TR)/20 PDN = 前一日的N值 TR= 當日的真實波動幅度 由於公式中需要前一日的N值,你在首次計算N的時候不能用這個公式,只能計算真實波動幅度的20日簡單平均值。

資金管理是海龜交易的精髓所在。

3、期望值

運用機率學公式,E= ∑iNp(xi)∗xi ,如果在有限次交易中,最後E的結果為正,那麼表示策略是可行的。所以短期的失利不重要,海龜以這樣的方式看待損失:損失只是做生意的成本,並不代表著一次錯誤交易或一個壞決策。

像海龜一樣思考

1、避免結果偏好

好的交易者考慮的是現在,而避免對未來考慮過多。新手則想預見到未來:如果他們贏了,他們會認為自己預測對了,感覺自己像個英雄;如果他們輸了,他們會把自己當成傻瓜。這是錯誤的。

2、避免近期偏好

大多數交易者不僅對未來考慮過多,對過去同樣考慮過多。他們會唸唸不忘過去所做的事,過去所犯的錯誤,還有過去那些失敗的交易。

海龜們會從過去的經歷中吸取教訓,但不會為過去而煩惱。他們不會為過去所犯的錯誤而責備自己,也不會對過去的失敗耿耿於懷。他們知道這只是遊戲的一部分。

3、避免預測未來

你必須從機率的角度來考慮未來,而不是擺出一副預測的架勢。文中舉的例子為對唐奇安趨勢系統的一次20年間月度回報率的測試結果,並作出了機率密度圖與累積機率圖,類似於常態分配圖。此例子提到了一個參數叫R乘數,R乘數等於一筆交易的利潤除以這筆交易的風險投入。風險投入的概念以一個例子說明。如果你在每盎司450美元的價位買了一份8月份黃金合約,止損退出價位是440美元,那麼你的風險投入是1000美元,因為450美元與440美元之差(10美元)乘以一份合約所代表的黃金數量(100盎司),等於1000美元。如果這筆交易賺了5000美元,那麼它就是一筆5R交易。以下是測試結果的機率圖(自己重設的,在資料上與文章的不一樣,意思是一個意思)

發現系統優勢

優勢這個詞借鑑自賭博理論,原本是指賭場所掌握的統計學優勢,它也指21點玩家可能通過記牌而獲得的優勢。在賭局中,如果你沒有優勢,從長期來看你肯定會輸。

交易世界中也是如此。如果你沒有優勢,交易的成本會讓你賠錢。交易中的優勢是指一種可以利用的統計學優勢,它以市場行為為基礎,而這些市場行為是會重複發生的。在交易世界中,最好的優勢來自於人類認知偏差所釀成的市場行為。

系統優勢三要素

1、資產組合的選擇:決定應該進入哪些市場的運算系統

2、入市訊號:決定什麼時候開始一筆交易的運算系統。

3、退出訊號:決定什麼時候退出一筆交易的運算系統。

如何發現優勢

當某種特定的市場行為發生時,系統會發出入市訊號。當你檢驗入市訊號時,你需要關注的是伴隨這種市場行為而來的價格變動。我們可以把這種價格變動分為兩個部分:好的變動和壞的變動。好的變動就是對你有利的變動。換句話說,如果你買入,那麼市場上漲就是好的變動。壞的變動與之相反。

交易者們把往壞方向的最大變動幅度稱為MAE(maximum adverse excursion),把往好方向的最大變動幅度稱為MFE(maximum favorable excursion)你可以用這些概念來直接衡量一個入市訊號的優勢。觀察一個入市訊號之後的價格變動,如果好的方向的平均最大變動幅度大於壞方向的平均最大變動幅度,說明存在正的優勢。一個真正隨機性的入市策略會帶來大致上相等的好變動和壞變動。比如,如果你用擲硬幣的方式來做出買賣決策,正面朝上就做多,背面朝上就做空,那麼在你入市之後的價格變動中,MFE與MAE應該相等。

MAE=abs(買入價-計算週期內的最低價)

MFE=abs(買入價-計算週期內的最高價)

文中介紹了作者自己開發的一種優勢比較工具,稱之為E-比率,E-比率的計算方式如下:

1、為每一個入市訊號計算指定時間段內的MFE和MAE。

2、將上述各MFE和MAE值分別除以入市時的ATR,這是為了根據波動性做出調整,將不同市場標準化。

3、將上述調整後的MFE和MAE分別求和然後除以入市訊號的總次數,得到調整後的平均MFE和MAE。

4、調整後的平均MFE除以調整後的平均MAE就是E-比率。

MFE= (∑1nMFEi/ATR)/n

MAE= (∑1nMAEi/ATR)/n

n = 入市訊號的總次數

E-比率 = MFE/MAE

擲硬幣的E-比率在n趨近於無窮大的時候應該等於1,所以E-比率等於1時,表明沒有優勢,也沒有劣勢。當E-比率大於1時(樣本量要足夠),說明當前的入市訊號存在優勢。E-比率要結合指定的時間段計算,如果以10個交易日為週期,那麼就稱之為E10-比率。文中將兩個入市訊號結合起來使用獲得了1.33的E-比率,證明通過一些策略是可以在買入時獲得很大優勢的。

E比率實驗:

由於A股市場無法做空,所以也無需擲硬幣,我的實驗步驟是這樣的:

1、找出A股的所有股票

2、隨機抽樣100隻股票

3、計算每隻股票從上市到當前時間的ATR值,採用海龜原版的方法計算ATR

4、針對每隻股票隨機選取起始時間(但是要保證往後有50個交易日)

5、計算每隻股票的E-50比率

6、計算這100隻股票的平均E-50比率

7、從第2步開始到第6步,重複100次

8、計算總體平均E-50比率

最後計算出來的E-50比率為1.49

同樣的邏輯計算出:

E-30比率為1.35

E-10比率為1.22

以下是程式碼片段

"""
根據隨機入市訊號計算E-50比率
"""
def randomERate(self, n=50):
    e_list = []
    st = storage()
    sql = "select symbol from stock_summary where type = 0"
    stockList = self.connect.getAll(sql)
    for _ in range(100):
        mae_list = []
        mfe_list = []
        sample = getSamplePointWithRandom(100, stockList)
        for ss in sample:
            symbol = stockList[ss][0]
            tableName = "stock_history_"+symbol
            if st.isTableExist(tableName):
                sql = "select `close`,`open`,`high`,`low` from "+tableName+" order by timestamp asc"
                dataList = self.connect.getAll(sql)
                if len(dataList) < n: #計算50日E比率,記錄小於50條的過濾掉
                    continue
                atr_list = []
                dList = []
                for idx in range(len(dataList)):
                    close_price = dataList[idx][0]
                    open_price = dataList[idx][1]
                    high_price = dataList[idx][2]
                    low_price = dataList[idx][3]
                    if idx == 0:
                        atr = high_price - low_price
                    else:
                        prev_close_price = dataList[idx-1][0]
                        tr = np.max([high_price - low_price, high_price - prev_close_price, prev_close_price - low_price])
                        atr = (19 * atr_list[idx - 1] + tr) / 20
                    atr_list.append(atr)
                    dList.append({'high': high_price, 'low': low_price, 'close': close_price, 'open': open_price, 'atr': atr})
                stop_num = len(dList) - n
                random_start = random.randint(0, stop_num)
                vList = dList[random_start:random_start+n]
                atr_buy = vList[0]['atr'] #入市時的atr
                v_low_list = []
                v_high_list = []
                for v in vList:
                    v_low_list.append(v['low'])
                    v_high_list.append(v['high'])
                mae = vList[0]['close'] - np.min(v_low_list) #買入價-最低價
                mfe = np.max(v_high_list) - vList[0]['close'] #最高價-買入價
                if atr_buy == 0:
                    continue
                mae_standard = mae/atr_buy
                mfe_standard = mfe/atr_buy
                mae_list.append(mae_standard)
                mfe_list.append(mfe_standard)
        mfe = np.sum(mfe_list)/len(mfe_list)
        mae = np.sum(mae_list)/len(mae_list)
        e = mfe/mae
        e_list.append(e)
    print(np.mean(e_list))

從實驗上可以看到,A股市場上的E比率通常是大於1的,由此可以看出,市場的不同會導致指標值出現較大的差異。至於這個指標對於A股市場,我認為需要辯證的看待。因為指標的演算法是MFE/MAE,而MFE是買入價減週期內最高價的絕對值,MAE是買入價減週期內最低價的絕對值。而我們的市場可能會出現這麼一種情況:在一個週期內出現了一段時間的暴漲和持續的低迷。如果我們的市場普遍存在這種情況,就會出現一個1.4以上的E比率。在《海龜交易法則》這本書中,這個值是一個非常有優勢的入市訊號,因為結合作者的交易策略(一次很大的盈利和N次較小的虧損,但是保證盈利蓋過所有虧損)是非常有效的,而對於A股市場來說,在沒有策略的指引下,這個指標還有意義麼?這是個值得思考的問題。所以對於所謂的技術導向來投機交易,學習指標沒有錯,但是我的意思是不能照本宣科,一是要理解指標的深層次含義,二是要結合具體的市場,三是要結合具體的策略,否則指標對我們來說不僅沒用,而且對我們有害。書中所說的也不能全信,重在理解,學習的只是思路,精髓在於利用思路去創造。

退出策略的優勢

如果有可能的話,即使是系統的退出策略也應該有優勢。遺憾的是,衡量退出策略的優勢更不容易。這是因為退出策略與入市策略和退出訊號都有關係。換句話說,你不能拋開最初建立一個頭寸的緣由而去單獨考察退出策略。這裡面不止有一個系統要素,而是有多個不同要素,而且它們之間有著錯綜複雜的互動作用。

尋找交易時機

優勢是在買者和賣者之間的戰場上發現的。作為一個交易者,你的任務就是找到這些戰場,靜觀誰勝誰敗。

對幾乎所有的交易策略來說,支撐和阻力都是一個基礎概念。所謂支撐和阻力,就是指價格有一種不突破前期水平的傾向。要理解這個概念,最簡單的辦法就是看看價格走勢圖。

支撐和阻力來源於市場行為,而這些市場行為則來源於三種認知偏差:錨定效應、近期偏好和處置效應。

錨定效應是指依賴輕鬆可得的資訊來判斷價格水平的傾向。一個近期的新高或新低變成了一個新的錨,之後的每一個價位都要根據這個錨來衡量和比較。新價格是高還是低,完全取決於它是高於還是低於這個錨定價。近期的高點或低點之所以會成為錨定價,是因為它們在圖表上一眼可見,在心理上對市場參與者有重大的影響。所謂的支撐位與阻力位即是錨定效應的一種表現。

大多數交易者都相信支撐和阻力現象的存在,這又進一步加強了支撐力和阻力,因為對這種現象深信不疑的交易者反過來也會促進這種現象。如果有很多交易者相信價格一旦下跌到某個水平就會有大批買入者接盤,那麼他們就更容易相信價格跌到這個水平時必然會反彈。這種信念將削弱他們在這個價位賣出的意願,因為他們更願意晚些再賣,等價格在這個支撐位反彈後再賣。於是,對支撐和阻力現象的篤定使它成了一種自我實現的機制。

支撐位和阻力位的突破

這裡就有趣了,支撐位和阻力位的突破對於兩種不同策略的交易者來說,他們的交易方向是相反的。既然叫阻力位和支撐位,那麼可以說價格在支撐位被支撐的機率比價格在支撐位被突破的機率要大,阻力位同理。那麼如果你是一個趨勢交易者,必然你在阻力位買入後的勝率就比較低,但是如果價格被突破,那麼錨定效應就會失效,此時價格可能會有一個大幅度的上漲。這樣在制定策略時,如果能設定一個低止損和高止盈,那麼你一次的大收益往往能彌補許多次的小虧損並有剩餘,從整體上來說就是盈利的。反趨勢交易者就是完全相反的操作,當你在支撐位買入後,價格反彈的機率會比較高,那麼你的勝率就會比較高,但是價格反彈後通常會馬上到達阻力位,到達阻力位之前你就必須賣掉頭寸,如果在支撐位買入後加個被突破,那麼就會虧損。與趨勢交易者一樣,你的策略也必須設定一個低止損和高止盈,但是這個高止盈不可超過阻力價位,這樣,如果你的止損和止盈是相同的百分比,那麼如果在勝率上有優勢,則總體上仍然是盈利的。

img

紅色是支撐位向下突破累積機率,藍色是壓力位向上突破累積機率

我在A股市場做了一個關於支撐位與壓力位的累計機率實驗。實驗隨機選取了500個股票樣本,每個股票隨機選取222個連續交易日,前22個交易日當做觀察週期,後200個交易日當做測試週期。先取觀察週期內的最高價和最低價,當做壓力位與支撐位。然後從測試週期的第一天開始測試,檢查當天的收盤價是否突破壓力位或支撐位,如果到測試週期的第三天,那就判斷第一天、第二天、第三天是否突破了壓力位或支撐位,以此類推。從圖中可以看出,12天內突破的機率大概是30%左右,25天內突破的機率大概是40%左右。說明錨定價位是真實存在的,並且支撐位的錨定價要比壓力位的錨定價效果更加顯著。支撐位與壓力位對於短線投機是非常重要的參考。

以下是程式碼片段:

def testSupportAndPress(self, n=1, limit=100):
    st = storage()
    period = self.getPeriod(n)
    sql = "select symbol from stock_summary where `type`=0"
    stockList = self.connect.getAll(sql)
    sample = getSamplePointWithRandom(500, stockList) #隨機抽取500個樣本
    support_result = {}
    press_result = {}
    for ss in sample:
        stock = stockList[ss]
        symbol = stock[0]
        tableName = "stock_history_"+symbol
        if st.isTableExist(tableName) is False: #沒有股票資料跳過
            continue
        sql = "select `low`,`high`,`close` from "+tableName+" order by timestamp asc"
        stockData = self.connect.getAll(sql)
        if len(stockData) < period+limit: #股票資料不符合測試條件跳過
            continue
        support_result[symbol] = {}
        press_result[symbol] = {}
        for day in range(limit):
            support_result[symbol][day + 1] = 0
            press_result[symbol][day + 1] = 0
        start_idx = random.randint(0, len(stockData)-period-limit)
        stockInfo = stockData[start_idx:start_idx+period+limit]
        periodData = stockInfo[0:period]
        obData = stockInfo[period:]
        period_low_list = []
        period_high_list = []
        for pd in periodData:
            low_price = pd[0]
            high_price = pd[1]
            period_low_list.append(low_price)
            period_high_list.append(high_price)
        support_price = np.min(period_low_list)
        press_price = np.max(period_high_list)
        ob_close_price_list = []
        for day in range(len(obData)):
            close_price = obData[day][2]
            ob_close_price_list.append(close_price)
            for ocp in ob_close_price_list:
                if ocp < support_price:
                    support_result[symbol][day+1] = 1
                    break
            for ocp in ob_close_price_list:
                if ocp > press_price:
                    press_result[symbol][day+1] = 1
                    break
    support_fresult = {}
    press_fresult = {}
    x = []
    support_y = []
    press_y = []
    for day in range(limit):
        support_fresult[day+1] = {'sum': 0, 'hit': 0}
        press_fresult[day+1] = {'sum': 0, 'hit': 0}
    for day in range(limit):
        x.append(day+1)
        for symbol in support_result.keys():
            support_fresult[day+1]['sum'] += 1
            support_fresult[day+1]['hit'] += support_result[symbol][day+1]
        for symbol in press_result.keys():
            press_fresult[day + 1]['sum'] += 1
            press_fresult[day + 1]['hit'] += press_result[symbol][day + 1]
        support_y.append(round(support_fresult[day+1]['hit']*100/support_fresult[day+1]['sum'], 2))
        press_y.append(round(press_fresult[day + 1]['hit']*100/press_fresult[day + 1]['sum'], 2))
    import matplotlib.pyplot as plt
    from matplotlib.font_manager import FontProperties
    font_set = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=12)
    from pylab import mpl
    mpl.rcParams['font.sans-serif'] = ['FangSong']
    mpl.rcParams['axes.unicode_minus'] = False
    from matplotlib.font_manager import _rebuild
    _rebuild()
    plt.title(u'A股市場支撐位與壓力位突破累積機率圖', fontproperties=font_set)
    ln1, = plt.plot(x, support_y, color='red', linewidth=2.0, linestyle='--')
    ln2, = plt.plot(x, press_y, color='blue', linewidth=3.0, linestyle='-.')
    plt.legend(handles=[ln1, ln2], labels=['n days support break', 'n days pressure break'])
    ax = plt.gca()
    ax.spines['right'].set_color('none')
    ax.spines['top'].set_color('none')
    plt.show()

衡量風險

風險有很多種,因此衡量風險的方法也有很多種。有些大風險產生於相對罕見的事件,10年內也碰不到幾次。有的風險較為常見,一年內總會出現幾次。大多數交易者都擔心4種主要的交易風險:

衰落:一連串損失使你的帳戶縮水。

低迴報:回報太低,你賺到的錢微不足道。

價格動盪:一個或多個市場中出現價格的驟然變動,導致無可挽回的重大損失。

系統死亡:市場狀態改變,致使曾經有效的系統突然失效。

當碰到衰落時,往往我們的大腦會產生類似下面的想法:

“萬一這個系統不再管用了,怎麼辦?”

“萬一這只是一個大滑坡的開端,怎麼辦?”

“萬一我之前的測試方法有問題,怎麼辦?”

等等,這些疑慮往往會促使一個新手放棄這個系統,或者開始有選擇的交易,以期“降低風險”。這經常導致交易者錯失本來可以賺錢的機會。在沮喪的情緒下,在看到初始帳戶遭受了1/2甚至更多的損失之後,交易者終於忍無可忍的退出了。這就是新手們即使使用有效的策略也無法賺錢的原因:他們高估了自己在高風險水平下承受巨大波動的能力。

風險的量化

1、最大衰落

測試期從最高點到最低點的下跌百分比。

2、最長衰落期

測試期從一個頂峰到下一個頂峰的時間,衡量一段損失期後需要多長時間才能重新站上新的高點。

3、回報標準差

回報標準差可以以不同週期為單位計算期間內平均回報的標準差。衡量回報率的起伏狀態。

4、R平方值

這個指標衡量的是實際投資回報率與平均複合增長率的吻合程度。對帶息帳戶這一類的固定收益投資來說,R平方值為1。(其實就是實際投資回報率與平均複合增長率做線性回歸後的R平方值)

回報的量化

平均複合增長率

公式: (1+r1)×(1+r2)×...×(1+rn)n−1

n代表有n期

r代表每期的收益率

例如初始投資資金為100元,第一個月收益100元,總資金變為200元,第二個月虧損100元,總資金又變為100元。

那麼如果按月算n = 2

r1 = (200-100)/100 = 100% = 1

r2 = (100-200)/200 = -50% = -0.5

平均複合增長率 = [(1+1)*(1-0.5)]^0.5 - 1 = 0

衡量風險與回報的綜合指標

夏普比率

這裡引入聚寬的夏普比率公式: (Rp−Rf)/σp

Rp 為年收益 = (1+P)250/n−1 ,P為週期總收益,n為週期天數

Rf 為無風險收益,如銀行利率,默認設定為0.04

σp 為策略收益波動率(標準差) = 250∗∑1n(rpi−rp)¯2/(n−1)

rp¯=∑1nrpi/n 每日收益率的平均值

rp為每日收益率

例如測算一個週期5天的夏普比率,假設初始資金為100元,第一天後,總資金為120,

第二天後,總資金為110元,第三天後,總資金為130元,第四天後,總資金為140元,第五天後,總資金為135元。

利用Python算算:

def getSharp(self):
    r_f = 0.04
    money = [100, 110, 130, 140, 135]
    r_p = (money[-1] - money[0])/money[0]
    numerator = r_p - r_f
    r_pi = []
    for idx in range(len(money)):
        if idx > 0:
            rp = (money[idx] - money[idx-1])/money[idx-1]
            r_pi.append(rp)
    r_p_mean = np.mean(r_pi)
    r_pi_diff_square = []
    for rp_item in r_pi:
        diff_square = np.square(rp_item - r_p_mean)
        r_pi_diff_square.append(diff_square)
    r_pi_diff_square_sum = np.sum(r_pi_diff_square)
    baidenominator = 250 * r_pi_diff_square_sum / len(r_pi_diff_square)
    sharp = round(numerator/baidenominator, 3)
    print(sharp)

最後計算的結果為:0.205

它代表每承受一單位總風險,會產生多少的超額報酬,by the way,上面的結果非常不錯。

MAR比率

MAR比率是Managed Accounts Reports有限公司發明的一個指標,這個公司專門提供對沖基金的業績報告。MAR比率等於年均回報率除以最大的衰落幅度,衰落是根據月末資料計算的*文中作者是通過整個週期的最高點到最低點的跌幅計算的*。這個比率是風險回報比率的一個相當快捷而又直接的衡量指標。文中作者提到,用它來剔除表現不佳的策略是非常有效的。對粗略的分析來說,它是一個絕好的工具。這句話我是這麼理解的,如果制定的策略MAR比率過低,那麼肯定可以淘汰。如果MAR不低,就可以進一步看別的指標了。

下面的程式碼是我隨機抽取一個三個月的時間段,然後隨機抽取的一些股票在這三個月的MAR比率,結果如下:

黑貓股份在時間段2013-12-17到2014-03-17的MAR比率是-0.0553287494628 大唐電信在時間段2013-12-17到2014-03-17的MAR比率是0.394967351489 永輝超市在時間段2013-12-17到2014-03-17的MAR比率是-0.0531024531025 麥捷科技在時間段2013-12-17到2014-03-17的MAR比率是0.00971195898055 泰山石油在時間段2013-12-17到2014-03-17的MAR比率是0.440798444327 光線傳媒在時間段2013-12-17到2014-03-17的MAR比率是0.0686396712033

MAR比率程式碼

def MAR(self):
    st = storage()
    timeRange = getRandomMonthTimestampRangeForLoopback(3)
    start_timestamp = timeRange[0][0]
    end_timestamp = timeRange[-1][-1]
    start_date = time.strftime('%Y-%m-%d', time.localtime(int(start_timestamp / 1000)))
    end_date = time.strftime('%Y-%m-%d', time.localtime(int(end_timestamp / 1000)))
    sql = "select symbol, name from stock_summary where type = 0"
    rList = self.connect.getAll(sql)
    id_list = getSamplePointWithRandom(10, rList)
    for idx in id_list:
        symbol = rList[idx][0]
        name = rList[idx][1]
        tableName = "stock_history_"+symbol
        if st.isTableExist(tableName) is True:
            sql = "select close, high, low from "+tableName+" where timestamp >= {st} " \
                                                                "and timestamp <= {et}".format(st=start_timestamp, et=end_timestamp)
            stockList = self.connect.getAll(sql)
            if len(stockList) <= 0:
                continue
            high_list = []
            low_list = []
            percent = (stockList[-1][0] - stockList[0][0]) / stockList[0][0]
            for item in stockList:
                high_price = item[1]
                low_price = item[2]
                high_list.append(high_price)
                low_list.append(low_price)
            high = np.max(high_list)
            low = np.min(low_list)
            wave = (high - low) / low
            MAR = percent/wave
            print("{name}在時間段{st}到{et}的MAR比率是{mar}".format(name=name, st=start_date, et=end_date, mar=MAR))

模倣傚應與系統死亡風險

在風險回報比率上擁有傲人記錄的那些策略往往都是最容易被整個行業群起模仿的策略。她們剛剛嶄露頭角,立即就被數十億美元竟相追隨,結果反而自毀長城,因為它們的規模已經超出了市場的承受能力。到頭來,它們早早就成了系統死亡的犧牲品。

在這一點上,套利策略可能是最好的例子。最純粹的套利實際上是一種沒有風險的交易。你在一個地方買入某個東西,在另一個地方把它高價賣掉,扣掉運輸或倉儲成本,剩下的都是你的李瑞。大多數套利策略都不會完全沒有風險,但有很多接近於沒有風險。問題是,靠這樣的策略賺錢是有前提條件的,那就是不同地方的同一種工具存在價格差,或者一種工具和另一種類似工具之間存在價格差。

使用某一種特定策略的人越多,價格差就消失的越快,因為這些交易者本質上都在爭奪同樣的機會。長此以往,這種效應會毀掉這種策略,因為它會變得越來越無利可圖。

風險與資金管理

文中作者的自己管理方式為

我們把頭寸分成一個個小塊,也就是我們所說的頭寸單位。每一個頭寸單位的合約數量是根據這樣的標準確定的:要讓1ATR的價格變動正好等於我們帳戶規模的1%。對一個100萬美元的帳戶來說,1%是10000美元。因此,我們會算出一個市場中代表著每份合約有1ATR變動幅度的美元金額,然後10000美元除以這個金額,得出每100萬美元的交易資本所對應的合約數量。我們把這些數字稱作頭寸單位。

我認為把這段話引用到A股市場應該這麼理解,假設我們的初始資金為20000元,有一隻股票A,當前的價格為P,當前的ATR為N,根據策略,此時股票A發出了買入訊號,那麼我們應該買入多少A呢?

要讓1ATR的價格變動正好等於帳戶規模的1%,我們的帳戶規模是20000,1%就是200。那麼200/N就是我們應該買入的股票數,由於股票買入是以手為單位的,所以如果200/N小於100,那麼我們不應該買入。設R=200/N,那麼R就為1個頭寸單位,不同股票因為價位和買入點的ATR均不同,所以1個頭寸單位所代表的股數也不同。頭寸單位同時受到買入價位和買入點的ATR影響。

通過這種方式,買入和賣出的資金管理可以做到一目瞭然。

海龜式交易系統

ATR通道突破系統

ATR通道突破系統是一個波幅通道系統,它把真實波動幅度均值(即ATR)用作波動性指標。350日移動平均收盤價加上7個ATR就是通道的頂部,減去3個ATR就是通道的底部。如果前一日的收盤價穿越了通道頂部,則在今日開盤時做多;如果前一日的收盤價跌破通道底部,則在開盤時做空(A股不存在做空)。當收盤價反向穿越了移動平均線,交易者們就會退出。

移動平均收盤價=N日收盤價之和/N

ATR的計算公式上面已經介紹過了,這裡不介紹了。

布林格(布林線)突破系統

這個系統的布林線是通過350日移動平均收盤價加減2.5倍標準差而得出的。如果前一日的收盤價穿越了通道的頂部,則在開盤時做多;如果前一日的收盤價跌破通道的底部,則在開盤時做空。

布林線的公式:

首先中間一條線是350日移動平均線MB = \sum_{1}^{n}{close}/n

然後計算校準差std= \sqrt{\sum_{1}^{n}{(close_{i}-mb)^{2}}/n}

頂部 = MB+2.5*std

底部 = MB-2.5*std

唐奇安趨勢系統

採用20日突破入市策略,10日突破退出策略。也就是價格向上突破20日均線,則買入。價格向下突破10日均線,則賣出。另外,這個系統規定了2ATR的止損退出點。2ATR的資金管理上面有介紹過,這裡不再贅述。

定時退出唐奇安趨勢系統

定時退出唐奇安趨勢系統是唐奇安趨勢系統的一個辯題,它採用的是定時退出策略,而不是突破退出策略。它在80天之後退出,沒有任何形式的止損點。

三重移動均線系統

這個系統使用三種移動均線:150日、250日和350日均線。交易者在150日均線穿越250日均線時買入(向上穿)或賣出(向下穿)。最長期的350日均線扮演的是趨勢過濾器的角色。只有150日和25日均線位於350日均線的同一側時才能交易。

歷史測試的謊言

歷史測試結果和實際交易結果的差異主要是由四大因素造成的:

交易者效應:如果一種方法在近期賺了很多錢,那麼其他交易者很可能會注意到它,開始用類似的方法模仿它,這很容易導致這種方法的效果不再想一開始那樣好。

文中有一例子說明:

幾年前曾有一個系統因為多年來的優異表現而變得大受歡迎,有很多經紀人開始向他們的客戶提供這個系統。我曾一度聽說已經有數億美元開始追隨這個系統。但就在它的影響力達到巔峰之後沒多久,它的追隨者們遭遇了一次曠日持久的衰落,而這樣長和這樣嚴重的衰落期在它20年的歷史測試中從未出現過。這個系統有一個容易被利用的軟肋。按照它的法則,如果當日的收盤價超過了某個特定水平,那麼就在次日早晨一開盤時買入或賣出。由於其他交易者知道什麼樣的價位會引發這些買單或賣單,那麼狠簡單,他們完全可以趕在當日收盤之前買入,然後在次日開盤之後馬上賣出。賣出價通常比買入價高得多,因為所有在一夜之間生成的買單都是在這個時候入市的,這是由系統的法則決定的。

交易者效應在任何情況下都有可能發生,不一定是某些交易者故意搶先行動的結果。只要有太多的交易者不約而同地試圖利用某種市場現象,這種現象的優勢就會被毀掉,至少在一段時間內不再有效,因為眾多交易者的定單會削弱它的優勢。

隨機效應:歷史測試的結果誇大了系統的內在優勢也可能是純隨機性的現象。

摘抄書中原文可以瞭解到任何系統的短期回測結果與任何基金的近幾年的成績都不足以讓我們做出正確的決策:

我曾對一個隨機性入市策略進行了模擬檢驗,這種策略僅根據電腦模擬的擲硬幣結果來決定在開盤時做多還是做空。當時我設計了一個完整的系統,採用以擲硬幣結果為基礎的進入策略和定時退出策略,在入市之後的若干天后退出,天數從20天到120天不等。然後我對這個系統作了100次測試。在這100次測試中,最好的一次獲得了16.9%的年均回報,在10.5年的測試期內把100萬美元變成了550萬美元,但最差的一次測試卻年均虧損20%。這說明,純隨機性事件可以導致巨大的差異。 如果加入一點優勢因素,結果會怎麼樣呢?如果我們在這個完全隨機性的系統中加入一個有正優勢的趨勢過濾器,那麼100次測試的平均表現會顯著改善。根據我的測試,平均回報率上升至32.46%,平均衰落幅度下降至43.74%。但即使加入了過濾器,各次測試結果之間仍有相當大的差異。在100次隨機測試中,最好的一次達到了53.3%的年回報率和1.58的MAR比率,最大的衰落只有33.6%;但最差的一次只有17.5%的回報率,最大衰落卻有62.7%之大。 當你用業績衡量指標去區分好基金和壞基金的時候,你很容易遭遇隨機效應問題。因為運氣好的平庸交易者要多於運氣不佳的優秀交易者。假設有1000個交易者,其中有80%接近於平均水平,只有五六個真正的高手。那麼,只有五六個人有可能成為運氣不佳的優秀交易者,卻有800個平庸的人有機會擁有好運。如果這800個人裡有2%能幸運的擁有10年的良好記錄,這意味著,擁有良好記錄的固然有21個人,但其中只有1/4的人是真正的優秀交易者。

最佳化矛盾:選擇特定參數的過程(比如選擇25日移動均線而不是30日移動均線),可能降低事後測試的預測價值。

有些交易系統需要用特定的數值進行計算,選擇這些數值的過程就是最佳化。這些數值被稱為參數。比如,長期移動均線的計算天數就是一個參數,短期均線的計算天數也是一個參數。最佳化就是為這些參數選擇最佳或最佳化數值的過程。

所謂的最佳化矛盾是有這麼一個觀點,在做回測時把參數調到最佳(也就是參數調到某個值的時候,回測期間受益最大,衰落較小)並不是最好的選擇,因為這樣可能導致過擬合,導致未來的回報與過去的回報產生相當大的差異。用文中作者的話來說,這個觀點純屬放屁。作者的觀點是應該將最佳參數應用到策略系統中去。作者寫了比較長的篇幅來論證他的觀點,我用幾句話來說明他的觀點:在統計學的角度來看,我們的一次回測結果其實是無數次回測結果中的一個隨機值,這個隨機值是無偏的。因此,我們的回測次數越多,回測結果就越接近真實的平均值。如果我們正在測試一個策略系統的某一個參數,這個參數取了不同的兩個值,每個值分別測試了十次,那麼肯定會有一個值的平均值產生的回報要高於另一個值產生的回報。由於每次回測結果可以看成獨立同分佈的,那麼實際上這個回測結果是一個常態分配,那麼未來利用此參數產生的回報就會落在我們回測結果平均值的某幾個標準差內。為什麼要選擇最佳的參數,因為無論未來回報是怎麼樣的,高回報均值的鄰域永遠比低迴報均值的鄰域要高,所以應該選最佳參數。

過擬合:系統可能太過複雜,以至於失去了預測價值。由於它與歷史資料的吻合度太高,市場行為的一個輕微變化就會造成效果的明顯惡化。

過擬合比較好理解,直接摘抄原文中的一段話即可理解:

過度擬合通常發生在系統變得過於複雜的時候。有時候,你可以通過新增法則來提高一個系統的歷史表現,但這僅僅是因為這些法則影響了屈指可數的幾筆重要交易。新增法則會導致過度擬合,這對發生在關鍵時期的交易來說尤其明顯。比如,假如一條法則要求你在接近最高峰的時候退出一個特別大的盈利頭寸,這當然會提高你的系統表現,但如果這條法則對其它情況沒有充分的適用性,這就成了過度擬合。

說簡單點,過擬合就是在回測時為了得到一個好的結果而特意針對某種場景設定一個條件。而通常的策略交易,應該具有普遍適用性。

歷史測試的統計學基礎

測試樣本的有效性

通過樣本特徵推斷總體特徵是統計學中的一個領域,也是歷史檢驗結果的未來預測價值的理論基礎。其中的核心觀點是,如果你有足夠大的樣本,你就可以用這個樣本的情況去近似推斷總體情況。因此,如果你對某一種特定交易策略的歷史交易記錄有充分的研究,你就可以對這種系統的未來潛力得出結論。

樣本分析在統計學上的有效性受兩大因素的影響:一個是樣本規模,一個是樣本對總體的代表性。

從概念上說,許多交易者和系統測試新手指導樣本規模的意思,但他們以為樣本規模僅指他們所測試的交易次數。他們並不明白,假如某個法則或概念僅適用於少數幾次交易,即使他們測試了上千次交易也不足以確保統計學上的有效性。

系統測試者假設過去的情況對未來的情況有代表性,如果這是事實,而且我們有足夠大的樣本,我們就可以從過去的情況中得出結論,並且把這些結論應用於未來的交易。但如果我們的樣本對未來不具代表性,那我們的測試就毫無用處。比如我們的市場通常有4種狀態,但我們的測試樣本只包含其中的某一種或兩種,那麼我們的樣本就不具有代表性。所以,你的測試方法必須儘可能的提高你所測試的樣本對未來的代表性。

找出穩健的衡量指標

在系統測試中,你要做的是觀測相對表現,分析未來潛力,判定一個特定理念是否有價值。但這裡面有個問題,那就是公認的那些業績衡量指標並不是非常穩定,也就是說,它們不夠穩健。這就使評判一個理念的相對優勢變得非常困難,因為寥寥幾次交易中的微小變化就能對這些不穩健指標的值產生巨大的影響。

如果對資料稍作改變並不會顯著影響一個統計指標,我們就說這個指標是穩健的。MAR比率、平均複合增長率(CAGR)和夏普比率用作相對表現的衡量指標。但這些指標並不穩健,因為它們對測試期的起始日和終止日非常敏感。以平均複合增長率為例,可以以時間為橫坐標,平均複合增長率的對數為縱坐標(這樣可以讓平均複合增長率關於時間的函數看起來像一個線性模型)做散點圖,你會發現當擷取不同時間段的時候,時間起止日的不同會使它們的連接線的斜率產生很大的變化,這說明一點點時間上的改變就對複合增長率有顯著的影響。如果我們將這些散點擬合成一條直線(使用一元線性回歸模型),取複合增長率時,我們取不同時間點上的擬合值,那麼這個擬合的複合增長率會穩定很多。文中作者對這個擬合的複合增長率取了一個新名字,叫回歸年度回報率,簡稱RAR。(regressed annual return)

img

平均複合增長率

上圖紅線代表實際的平均複合增長率,藍線是通過最小二乘法擬合的平均複合增長率,也就是RAR,可以看到,對於實際的平均複合增長率,如果拉長或縮短橫坐標的範圍,Y值兩點連線的斜率會有較大的變化,這代表指標是不穩健的,如果將它擬合成一條直線,斜率不變,所以更加穩定。指標值對於實際值的殘差較小,表明擬合值不會有太大的誤差。

RAR程式碼如下:

def CAGRlog(rate):
    r = []
    for ra in rate:
        r.append(np.log(1+ra))
    return np.sum(r)

def testCAGRLog(self):
    p_list = np.random.rand(10)
    X = []
    Y = []
    for idx in range(1, len(p_list)+1):
        X.append(idx)
        pl = p_list[:idx]
        result = CAGRlog(pl)
        Y.append(result)
    import statsmodels.api as sm
    X_fit = sm.add_constant(X)
    model = sm.OLS(Y, X_fit)
    results = model.fit()
    Y_fit = results.predict()
    import matplotlib.pyplot as plt
    from matplotlib.font_manager import FontProperties
    font_set = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=12)
    from pylab import mpl
    mpl.rcParams['font.sans-serif'] = ['FangSong']
    mpl.rcParams['axes.unicode_minus'] = False
    from matplotlib.font_manager import _rebuild
    _rebuild()
    plt.title(u'平均複合增長率', fontproperties=font_set)
    ln1, = plt.plot(X, Y, color='red', linewidth=2.0, linestyle='--')
    ln2, = plt.plot(X, Y_fit, color='blue', linewidth=2.0, linestyle='--')
    plt.legend(handles=[ln1, ln2], labels=['actual value', 'fit value'])
    ax = plt.gca()
    ax.spines['right'].set_color('none')
    ax.spines['top'].set_color('none')
    plt.show()

如果將夏普比率的分子替換成RAR,那麼夏普比率同樣變成了穩定性指標。

替換MAR比率的穩健風險回報比率,作者給它起名叫R立方(robust rist/reward ratiio)。R立方的分子就是RAR,分母是個新指標,作者稱之為長度調整平均最大衰落。這個分母指標有兩個要素:平均最大衰落和長度調整。

平均最大衰落就是5次最大衰落幅度的平均值。長度調整就是將這5次衰落期的平均天數除以365天,然後用這個結果乘以平均最大衰落。平均衰落天數的計算原理與平均衰落幅度相同,也就是將5次衰落期的天數相加再除以5。

設5次最大的衰落幅度分別為a, b, c, d, e,那麼平均最大衰落 = (a+b+c+d+e)/5

設5次最大的衰落幅度的天數分別為u, v, w, x, y,那麼長度調整 = (u+v+w+x+y)/(5*365)

穩健風險回報比率的分母 =[ (a+b+c+d+e)/5] * [(u+v+w+x+y)/(5*365)]

從虛擬測試到實戰交易

怎麼判斷你在實際交易中可能獲得什麼樣的成果?對歷史測試來說,這或許是最有趣的問題之一。

要想得到有意義的答案,你必須理解影響系統表現的因素,使用穩健指標的必要性,以及採集足夠大的代表性樣本的重要性。一旦你做到了這一點,你就可以開始思考市場變換的潛在影響,思考為什麼連老練的交易者設計的優秀系統也會經歷業績的盛衰起伏。你不可能知道,也不可能預見到一個系統的表現會怎麼樣,這是現實。充其量,你只能借用有效的工具來判斷系統的潛在效果,以及影響這種效果的因素。

如果一個系統在最近一段時間表現得特別出眾,這有可能是個運氣問題,或許市場對這種系統來說正處於理想的狀態中。一般來說,這種冒尖的系統在好時期過後很容易轉入困難時期,不能指望它在未來會重現這種好運的表現。這也許會發生,但你不能寄希望於運氣。你更有可能經歷業績的下滑。

參數檢驗調整

1、在決定採用一個系統之前先體驗一下參數的作用是個好習慣。挑出幾個系統參數,大幅調整參數值,然後看看效果怎麼樣?如果效果顯著,說明系統參數對於系統的影響是顯著的。

2、隨便選擇8-10年前的一天,用這一天之前的所有的資料進行最佳化,當你得出了最佳化參數值後,再用這一天之後兩年內的資料檢驗一下這些參數值。這就跟機器學習中要建立訓練集和測試集一樣的道理。

蒙特卡洛檢驗

摘抄自wiki的對蒙特卡洛檢驗的一個簡單說明:

假設我們要計算一個不規則圖形的面積,那麼圖形的不規則程度和分析性計算(比如,積分)的複雜程度是成正比的。蒙特卡羅方法基於這樣的想法:假設你有一袋豆子,把豆子均勻地朝這個圖形上撒,然後數這個圖形之中有多少顆豆子,這個豆子的數目就是圖形的面積。當你的豆子越小,撒的越多的時候,結果就越精確。藉助電腦程序可以生成大量均勻分佈坐標點,然後統計出圖形內的點數,通過它們佔總點數的比例和坐標點生成範圍的面積就可以求出圖形面積。

文中所用的利用蒙特卡洛檢驗思想的一種方法是拼接淨值曲線:在初始淨值曲線中隨機選擇一些小段,然後將它們打亂次序組成一個新的淨值曲線。按如此方式生成幾千個新的曲線,然後計算曲線對應的RAR,通過RAR的直方圖和累積機率圖來找出90%置信區間下的RAR的水平。

以上詳細闡述了《海龜交易法則》這本書的所有觀點、思路及具體操作,下面給出原版海龜交易系統。以下均摘抄原文:

入市策略 交易者大多從入市訊號的角度來評判一個特定的交易系統。他們相信,入市策略是一個交易系統最重要的一個環節。 所以他們可能想不到,海龜們使用的是一個以理查德-唐奇安的通道突破系統為基礎的非常簡單的入市系統。 海龜們使用兩個有所差異但也彼此相關的入市系統,我們稱為系統1和系統2。我們可以自由決定如何在這兩個系統之間分配資金。有的海龜只用系統2,有的在兩個系統上各投入50%的資金,還有的採用其他組合。這兩個系統分別是: 系統1:以20日突破為基礎的短期系統 系統2:以55日突破為基礎的長期系統 突破 突破是指價格超越了過去一定時期內的最高點或最低點。所以,20日突破就是指價格超越了過去20天的最高或最低點。 海龜們總是在突破發生時立即入市交易,不會等到當日收盤或次日開盤時。在跳空開盤的情況下,假如開盤價已經跳過了突破價,海龜們就在開盤時入市。 系統1入市法則 只要價格超越20日最高或最低點一個最小單位,海龜們就馬上行動。如果價格超越了20日高點,海龜們就買入1個頭寸單位,開始做多。如果價格跌破了20日低點,海龜們就賣出1個頭寸單位,開始做空。 但是,假如上一次突破是一次盈利性突破(也就是可以帶來一次盈利的交易),那麼系統1的當前入市訊號將被忽略。注意:對這一法則來說,上一次突破就是指市場的上一次實際突破,不管交易者當時採取了突破交易還是根據這一法則而忽略了那次突破。那麼什麼是虧損型的突破呢?如果突破日之後的價格在頭寸有機會退出獲利(根據10日突破退出法則)之前發生了2N幅度的不利變動,這就被視為一次虧損性的突破。 對這一法則來說,上一次突破的方向並不重要。因此,無論上一次突破是向上突破還是向下突破,只要是虧損型突破,那麼目前的新突破(無論是向上還是向下突破)就會被視為有效的入市訊號。 不過,如果一次突破因為這條法則而被忽略,那麼交易者將在55日突破點入市,這是為了避免錯過重大趨勢。這個55日突破點被視為一個保障性突破點。 在任何時候,如果一個交易者處於離場等待的狀態,那麼總有某個價位能引發空頭入市訊號,也總有某個更高的價位能引發多頭入市訊號。如果上一次突破是虧損的,那麼新突破(也就是20日突破點)將更接近於當前價格;如果上一次突破是盈利性的,那麼新突破點可能離當前價遠的多,因為那有可能是個55日突破點。 系統2入市法則 只要價格超越55日最高點或最低點一個最小單位,我們就入市。如果價格超越了55日高點,海龜們就買入1個頭寸單位,開始做多。如果價格跌破了55日低點,海龜們就賣出1個頭寸單位,開始做空。 對系統2來說,所有突破都被視為有效訊號,無論上一次突破是虧損性還是盈利性的。 逐步建倉 海龜們首先在突破點建立1個單位的頭寸,然後按1/2N的價格間隔一步一步擴大頭寸。這個1/2N的間隔以上一份定單的實際成交價格為基礎。所以,如果最初的突破交易發生了1/2N的成交價偏差,那麼新定單的價格將與突破點相差1N,也就是最初1/2N的偏差加上1/2N的標準間隔。 這個過程將繼續下去,一直到頭寸規模達到上限。如果市場足夠活躍,我們有可能在一天內加滿4個頭寸單位。 請看下面的例子: 黃金 N=2.5 55日突破價=310.00 第一個單位:310.00 第二個單位:310.00+2.5=311.25 第三個單位:311.25+1/22.5=312.50 第四個單位:312.50+1/2*2.5=313.75 止損標準 海龜們根據頭寸風險來設定止損標準。任何一筆交易的風險程度都不得超過2%。 由於1N的價格變動代表著帳戶淨值的1%,在2%的風險限制下,價格變動的上限就是2N。海龜們止損標準就是2N:對多頭頭寸來說,止損價比入市價低2N;對空頭頭寸來說,止損價比入市價高2N。 為了把整體頭寸風險控制在最低水平,如果我們(按1/2N的價格間隔)後續補充了頭寸單位,那麼之前頭寸單位的止損點將相應的調整1/2N。一般來說,這意味著整個頭寸的止損點將與最新新增的頭寸單位相距2N的距離。不過,如果頭寸補充的價格間隔因為市場變化過快或開盤跳空等情況而大於1/2N,止損標準也會有所變化。 下面是一個例子: 原油: N=1.2 55日突破價=28.30 入市價/止損價 第一個單位 28.30/25.90 diff=2.4=2N --------------------------------------------- 第一個單位 28.30/26.50 diff=1.8=1.5N 第二個單位 28.90/26.50 diff=2.4=2N 第一個頭寸單位和第二個頭寸單位買入價相差1/2N=0.6 --------------------------------------------- 第一個單位 28.30/27.10 diff=1.2=1N 第二個單位 28.90/27.10 diff=1.8=1.5N 第三個單位 29.50/27.10 diff=2.4=2N --------------------------------------------- 第一個單位 28.30/27.70 diff=0.6=1/2N 第二個單位 28.90/27.70 diff=1.2=1N 第三個單位 29.50/27.70 diff=1.8=1.5N 第四個單位 30.10/27.70 diff=2.4=2N 假如第四個單位因為市場跳空高開在每股30.80美元成交,那麼結果將變為: 第一個單位 28.30/27.70 diff=0.6=1/2N 第二個單位 28.90/27.70 diff=1.2=1N 第三個單位 29.50/27.70 diff=1.8=1.5N 第四個單位 30.80/28.40 diff=2.4=2N 備選止損策略:雙重損失 海龜們也學習了另外一種叫做雙重損失的止損策略。 在雙重損失策略下,每一筆交易的風險上限不是2%,而是0.5%。也就是說,價格波動的上限是1/2N。在一個頭寸單位止損退出後,交易者將在價格恢復到最初的入市價時重新建立這個單位。 比如,如果採用雙重損失止損策略,上述原油交易將變成下面的樣子: 原油: N=1.2 55日突破價=28.30 入市價/止損價 第一個單位 28.30/27.70 diff=0.6=1/2N --------------------------------------------- 第一個單位 28.30/27.70 diff=0.6=1/2N 第二個單位 28.90/28.30 diff=0.6=1/2N --------------------------------------------- 第一個單位 28.30/27.70 diff=0.6=1/2N 第二個單位 28.90/28.30 diff=0.6=1/2N 第三個單位 29.50/28.90 diff=0.6=1/2N --------------------------------------------- 第一個單位 28.30/27.70 diff=0.6=1/2N 第二個單位 28.90/28.30 diff=0.6=1/2N 第三個單位 29.50/28.90 diff=0.6=1/2N 第四個單位 30.10/29.50 diff=0.6=1/2N 退出(止盈和止損雙重標準) 系統1採用10日突破退出法則:對多頭頭寸來說,在價格跌破過去10日最低點時退出;對空頭頭寸來說,在價格超過10日最高點時退出。總之,如果價格發生了不利於頭寸的10日突破,所有頭寸單位都要退出。 系統2則採用20日突破退出法則:對多頭來說2是日0向下突破,對空頭來說是20日向上突破。只要價格發生了不利於頭寸的20日突破,所有頭寸單位都會退出。

以下是我用系統入市法則2,在A股市場上做的回測。由於原版的海龜交易法則是基於期貨市場執行的,與A股市場有較大的差異,所以我也只能儘量貼合原版的規則,而無法做到一模一樣。以下是我的改編版規則:

假設初始資金為1000000(100萬)

1、找出近55日的最高價。

2、計算當日的ATR值。

3、如果當日收盤價大於55日的最高價一個ATR,就產生一個入市訊號。

4、如果a股產生了一個入市訊號,那麼就買入100萬 * 0.01 / (ATR * 100) 手(向下取整)該股股票。

5、買入的股票不重複,比如頭一天買入了a股,第二天a股繼續突破了55日最高價,那麼將不買入此股票。

6、如果找到了多個產生入市訊號的股票,則按順序買入,直到資金不足為止。

7、退出機制:設自買入之日算起到當前日前一天裡的最高收盤價為H,那麼如果當前收盤價-H <= -1 * ATR,就賣出此股票。

奉上聚寬程式碼

# 匯入函數庫
from jqdata import *
import numpy as np
import math
import copy
# 初始化函數,設定基準等等
def initialize(context):
    # 設定滬深300作為基準
    set_benchmark('000300.XSHG')
    # 開啟動態復權模式(真實價格)
    set_option('use_real_price', True)
    # 輸出內容到日誌 log.info()
    log.info('初始函數開始運行且全域只運行一次')
    # 過濾掉order系列API產生的比error等級低的log
    # log.set_level('order', 'error')
    g.all_stock = get_all_securities(types=['stock'], date=None).index.values
    g.result = []
    ### 股票相關設定 ###
    # 股票類每筆交易時的手續費是:買入時佣金萬分之三,賣出時佣金萬分之三加千分之一印花稅, 每筆交易佣金最低扣5塊錢
    set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')

    ## 運行函數(reference_security為執行階段間的參考標的;傳入的標的只做種類區分,因此傳入'000300.XSHG'或'510300.XSHG'是一樣的)
    # 收盤時運行
    run_daily(market_close, '14:50', reference_security='000300.XSHG')
    
def sellSignal(context):
    for stock in context.portfolio.positions.keys():
        hit_record = None
        for item in g.result:
            if item['stock'] == stock:
                hit_record = item
                break
        if hit_record is None:
            break
        stock_price_info = get_price(stock, start_date=hit_record['t'], end_date=context.current_dt, frequency='daily', fields=['open', 'close', 'high', 'low'], skip_paused=True, fq='pre', fill_paused=False)
        if stock_price_info.empty is True:
            continue
        stock_price_info = stock_price_info.iloc[:stock_price_info.shape[0]-1, ]
        if stock_price_info.empty is True:
            continue
        stock_price_info = stock_price_info.dropna(axis=0, how='any')
        if stock_price_info.empty is True:
            continue
        close_list = stock_price_info['close'].tolist()
        break_price = np.max(close_list)
        cur_price = context.portfolio.positions[stock].price
        #當前價減去買入後的最高價小於或等於1個入市時的ATR時退出
        if cur_price - break_price  <= hit_record['atr'] * -1:
            closeable_amount = context.portfolio.positions[stock].closeable_amount
            order_res = order(stock, closeable_amount * -1)
            if order_res is not None:
                if order_res.status == OrderStatus.held:
                    g.result.remove(hit_record)

def buySignal(context):
    for stock in g.all_stock:
        cpp_value_list = []
        for cpp in context.portfolio.positions.keys():
            cpp_value_list.append(context.portfolio.positions[cpp].value)
        if len(cpp_value_list) > 0:
            if context.portfolio.available_cash < np.min(cpp_value_list):
                break
        position_stock_list = context.portfolio.positions.keys()
        #如果這只股票已經持倉了,則跳過
        if stock in position_stock_list:
            continue
        stock_price_info = get_price(stock, count=100, end_date=context.current_dt, frequency='daily', fields=['open', 'close', 'high', 'low'], skip_paused=True, fq='pre', fill_paused=False)
        sumRow = stock_price_info.shape[0]
        last_stock_price_info = get_price(stock, count=1, end_date=context.current_dt, frequency='1m', fields=['open', 'close', 'high', 'low'], skip_paused=True, fq='pre', fill_paused=False)
        last_stock_close_price = last_stock_price_info.iloc[0,:]['close']
        last_stock_high_price = last_stock_price_info.iloc[0,:]['high']
        last_stock_low_price = last_stock_price_info.iloc[0,:]['low']
        last_stock_pdc = stock_price_info.iloc[stock_price_info.shape[0]-2, :]['close']
        if math.isnan(last_stock_close_price) or math.isnan(last_stock_high_price) or math.isnan(last_stock_pdc):
            continue
        if sumRow - (55 + 1) >= 0:
            start_idx = sumRow - (55 + 1)
            #獲取當前交易日的前一天到前55天的資料
            period_data = stock_price_info.iloc[start_idx:sumRow-1, :]
            #過濾掉停牌的日子
            period_data = period_data.dropna(axis=0, how='any')
            #前55天再往前推20個交易日的資料,用來計算前55天裡,第一天的ATR
            if start_idx - 20 >= 0:
                pre_atr_stock_df = stock_price_info.iloc[start_idx-20:start_idx, :]
            else:
                pre_atr_stock_df = stock_price_info.iloc[:start_idx, :]
            #過濾掉停牌的日子
            pre_atr_stock_df = pre_atr_stock_df.dropna(axis=0, how='any')
            pre_atr_list = []
            for pas_row in range(pre_atr_stock_df.shape[0]):
                high_price = pre_atr_stock_df.iloc[pas_row, :]['high']
                low_price = pre_atr_stock_df.iloc[pas_row, :]['low']
                if pas_row == 0:
                    atr = high_price - low_price
                else:
                    pdc = pre_atr_stock_df.iloc[pas_row-1, :]['close']
                    compare_list = [high_price-low_price, abs(high_price-pdc), abs(pdc-low_price)]
                    atr = np.max(compare_list)
                pre_atr_list.append(atr)
            #第一天的ATR
            start_atr = np.mean(pre_atr_list)
        else:
            #如果沒有前55天的資料,那麼就直接選第一天資料到倒數第二條資料,並且用第一天的最高價-最低價當做ATR
            period_data = stock_price_info.iloc[:sumRow-1, :]
            period_data = period_data.dropna(axis=0, how='any')
            start_atr = period_data.iloc[0, :]['high'] - period_data.iloc[0, :]['low']
        atr_list = [start_atr]
        #算前55天內,每天的ATR
        for prow in range(1, period_data.shape[0]):
            high_price = period_data.iloc[prow, :]['high']
            low_price = period_data.iloc[prow, :]['low']
            pdc = period_data.iloc[prow-1, :]['close']
            tr = np.max([high_price-low_price, abs(high_price-pdc), abs(low_price-pdc)])
            cur_atr = (19 * atr_list[prow-1] + tr) / 20
            atr_list.append(cur_atr)
        #入市時的atr,也就是最後一天的ATR
        last_atr = (19 * cur_atr + np.max([last_stock_high_price-last_stock_low_price, abs(last_stock_high_price-last_stock_pdc), abs(last_stock_low_price-last_stock_pdc)])) / 20
        #55天內的最高價
        high_list = period_data['high'].tolist()
        if len(high_list) > 0:
            break_price = np.max(high_list)
            #如果當前價-55日最高價大於等於1個ATR,則入場
            if last_stock_close_price - break_price >= last_atr:
                #計算頭寸單位
                hand = int(int(context.portfolio.starting_cash) * 0.01/(last_atr * 100))
                order_res = order(stock, hand*100)
                if order_res is not None:
                    if order_res.status == OrderStatus.held:
                        #買入成功後,記錄入市時的ATR等資訊
                        g.result.append({'stock': stock, 't': copy.deepcopy(context.current_dt), 'order_id': order_res.order_id, 'atr': last_atr})
    
def market_close(context):
    buySignal(context)
    sellSignal(context)
        
    

由於時間有限,以下是隨機時間段做了兩次回測的結果,時間週期為三個月

img

2011年3月1日到2011年6月1日

img

2014年1月15日到2014年4月15日

從結果來看,效果並不理想,說明市場不同,法則也不同,切莫死記硬背,拿來就用。但是海龜的交易思想是值得借鑑的。總結一下海龜交易的思想:

1、制定交易策略,制定交易策略主要分幾個方面:一、發現市場優勢,也就是我們得平衡勝率和單次收益,如果勝率高,對於單次收益的要求就可以適當降低,如果勝率低,則相反,可以通過計算尋找一個好的平衡點。二、使用風險衡量指標,對我們的策略進行風險衡量。三、資金管理,可以使用海龜的ATR法,也可以使用等量資金。資金管理的方式很多,主要的目的就是控制風險,我們能清楚的知道每次虧損的最大額度,我們能清楚的知道我們能承受最大的連續虧損是多少次,我們能清楚的知道我們破產的機率是多大。

2、有效回測,要把回測的時間定在一個較長的範圍內,這樣既可以覆蓋市場所有的狀態,也可以避免前文所說的譬如近期偏好的一些錯誤。然後要進行隨機性的多次測試,用測試的術語來說就是,我們要把用例覆蓋全,儘量做全量測試,儘量把邏輯的每個分支都覆蓋到。用統計學術語來說就是,如果我們的樣本足夠多,我們離真相就越近。這樣我們計算出來的風險指標就更有說服力,我們進行交易時的信心就會更足。

3、也就是心理,在我看來,你說你是價值投資也好,量化交易也罷,投機也行,反正都是炒股,只是流派風格不同,就像有的人使刀,有的人使劍,本身不存在對錯,在各自的領域都有佼佼者,也有垃圾,說到底,價值投資、量化交易只是拿來賺錢的工具。差勁的劍客,活不過幾天;合格的劍客,能立足於江湖;卓越的劍客,能笑傲江湖。如果你有一本劍譜,練熟它,相信它,執行它,那麼起碼能活著,可能還活的不錯。如果你能寫一本劍譜,那就能在這個江湖呼風喚雨,要尊嚴有尊嚴,要女人有女人。我本人也還處於找劍譜的路上,但我相信是這麼個道理,以上。


以上內容如有誤歡迎各位指出,本人也是憑自己有限的能力想儘量解釋清楚海龜交易法則的詳細思想和操作方法。

市場上總是不乏各種選股或尋找買賣點的方法,都很值得參考,但八年級、自稱「股海筋肉人」的黃健維,卻以自身所受的教訓認為「學而不思則罔」,除了虛心向外學習,還要依循實戰經驗,思考自己適用的補強策略,才能有效提高致勝率。而他的「思」,則是一套簡單卻又十分關鍵的資金控管法。

追逐強勢股 三步驟降低踩地雷、住套房風險

股海筋肉人的作法,是以追逐強勢股為主,當股價突破短線高點、站上所有均線、甚至盤中漲幅超過五%,都是他設定買進的條件。看起來像是單純「追高」,其實透過以下三個步驟,已經將「買到地雷股」、「追高套牢」的機率大幅降低。

第一步,建立股票池。這部分股海筋肉人以基本面為主,依營收與獲利表現選取相對績優的股票,「其實這些條件算是很寬鬆,所以我的股票池通常有上百檔股票可供選擇。」

他所謂的「寬鬆條件」,包括

  • 單月營收年增率連續三個月為正值,最好是剛由負轉正;
  • 最近一季的EPS(每股盈餘)年增率大於零,而且過去四季每季的EPS都大於零;
  • 市值在二十億元以上。

建立股票池後,進入第二步的選股階段。

  • 若股票池內的標的其盤中漲幅超過五%或突破短期高點,除了追蹤該股,也會立刻檢視這檔股票所屬的類股或族群,是否有同步發動或上漲的跡象,藉此找出該股所屬族群是否同步發動,「具有族群性,出現波段漲勢的機會比較高。」

  • 鎖定目標,但還不要急著進場,股海筋肉人此時會先透過自行設定的資金控管公式,決定買進張數,這也是他整套操作的精華所在。

在各項設定條件中,他通常以月線為支撐或停損的基本指標,並且依據總體資金部位設定停損比率與金額,接著再計算進場價位到停損點之間的價差距離。有了「跌到停損點的價差」和「可忍受的虧損金額」後,也就能推算出「在風險受控下,可以買進的張數」。

以友達為例,假設現在友達符合他設定的買進標準,目前股價是九元,月線在八.四元;若投資人手中有一百萬元,可以設定這筆交易能承受的停損幅度是一.五%,也就是這筆交易最多可以賠一萬五千元。那麼,可以算出能買進的友達張數是二十五張。買進後,只要沒有跌破月線就一路抱著,跌破月線就迅速出場,不戀棧。

  • 股海筋肉人的資金控管算式:可承受虧損金額,除以進場價位至停損點的價差。以文中的友達為例,算式為:15000÷(9-8.4元)=25000股=25張。

嚴控風險 確保虧損有上限

股海筋肉人表示,這套資金控管方法的重點在於**「買進價位離支撐點愈近(遠)、可以買的張數就愈多(少)」**,這符合他對風險控管的要求,再搭配正確選股,至少可以做到虧損不擴大,進而達到大賺小賠、資金部位逐漸成長。

這套計算其實不難,股海筋肉人在[Excel](https://www.businesstoday.com.tw/article/category/80392/post/201912260034/算股達人 用Excel化繁為簡算出飆股 自動儲存功能 再也不用怕當機)中設好公式、輸入關鍵數字馬上就可得知。他提醒,支撐或停損點也可以運用其他均線或重要型態,例如前波轉折低點,「只要標準一致就可以。」

他認為,不少投資人很會選股、買點抓得也不錯,但若沒有做好資金控管、嚴守交易紀律,很容易「賺十筆、賠一筆」,而賠的這一筆就把先前賺的全吐光,導致財富難以累積,「實在太可惜。」

靠著這套結合基本面、技術面、族群性與資金控管等步驟,股海筋肉人踏入股市以來,除了一八年之外,每年都擁有兩位數以上的報酬率,尤其是隨著本金逐漸增加,近三年就翻了兩倍。以今年來說,他持有的聰泰獲利超過六成、創惟、愛普等也都有斬獲。

策略優化 – 如何避免過擬合?

出處:https://www.finlab.tw/backtesting-overfitting-probability/

thumbnail 8 1

當你做回測做久了,就會發現,找到「歷史報酬率」好的策略很簡單,但是找到「未來報酬率」好的策略非常難。原因在於做了過多的參數枚舉與優化,當樣本數夠大,自然會有極端的數據產生,就像是夜路走多了會碰到鬼,人多必有白痴,樹多必有枯枝,就像是量子力學中,波函數坍縮成我們所處的現實,代表著均值,但在極端的多重宇宙樣本中,你也有可能是總統,代表著眾多巧合下的極端事件。

1 本文就是用口語的方式,帶你瞭解如何判斷過擬合的演算法

2 我們很有可能是優化雜訊,而非優化價格的規律。

2.1 要怎麼辨別這兩者的不同呢?

2.2 聽不懂?別走,讓我舉個例子

3 回到策略的角度,如何驗證「貨真價實的策略」?

4 所以比較好的方法是?

5 1. 確定參數效果真的比較好

6 2. 產生多重的 IS 跟 OOS

7 實驗結果:

本文就是用口語的方式,帶你瞭解如何判斷過擬合的演算法

牛頓從蘋果落地的現象,發現了萬有引力,F=ma,因為實驗的雜訊很小(風、熱能散失等等),才能有經典簡潔的公式,然而把牛頓的實驗,換到財經領域時,可能就不是這麼管用了,當我們在建模時,價格的雜訊遠大於規律,

我們很有可能是優化雜訊,而非優化價格的規律。

要怎麼辨別這兩者的不同呢?

我們可以先從直觀的角度出發,究竟歷史上成功的偉人,Bill Gates、Steve Jobs、Elon Musk,這些科技巨擘,他們之所以能夠有今天的成就,是一連串的巧合,還是他們有一些人格特質,促使他們的成功?另一個極端的例子,假如今天某人中了樂透彩而一夕爆富,那很明顯,他很可能是多重宇宙中,非常成功的一個版本,但他的成功,可能並非來自他的人格特質,而是來自運氣。今天就運氣跟命運,來討論策略模型過擬合的問題。

模型的過擬合,就像是簽樂透彩,只要參數夠多了,總會中獎。所以策略績效好,究竟是不是運氣好,最重要的事,就是要確保「實驗是有效的」。如何定義實驗是有效的呢?

樣本內的「最佳參數」在樣本外的「績效」也是顯著的機率是高的

聽不懂?別走,讓我舉個例子

這邊的「樣本內」就是指我們現在的現實世界,已經發生了的事情,例如我們知道 Steve Jobs 是頻果創辦人,將科技與時尚結合,促進科技的進步功不可沒,所以他就是我們在樣本內的「最佳人選」。而「樣本外」就是指那些我們沒見過的多重宇宙,在多重宇宙中,假如都有 Steve Jobs 這個人,而他都有一番豐功偉業,那就代表那是他貨真價實的實力。

回到策略的角度,如何驗證「貨真價實的策略」?

最簡單的方式,一般我們會使用 hold-out,將歷史資料分成樣本內(in-sample IS)和樣本外(out-of-sampe OOS)並且在 IS 做最佳化後,再用 OOS 驗證,這樣的方法有幾個缺點:

  • 由於 IS 跟 OOS 都是人為定義,所以當重複優化很多次後,還是會對於 OOS 的績效越來越熟悉,最後不免還是用了 OOS 的資訊來設計模型
  • 資料量的問題,回測跟驗證都需要一定長度的歷史數據,將歷史數據分成 IS 跟 OOS 顯然有點浪費
  • OOS 通常是時間序列的尾端,代表近期的資料,對於策略效能有決定性的影響,然而卻不能拿來開發

所以比較好的方法是?

The probability of backtest overfitting的作者是這樣做的: Bailey, D.H., Borwein, J., Lopez de Prado, M. and Zhu, Q.J., 2016. The probability of backtest overfitting. Journal of Computational Finance, forthcoming.

1. 確定參數效果真的比較好

任何策略都有好與不好的時候,要確定參數效果很好,不是看績效是正的就好,而是要跟其他參數的績效作比較, 例如最近臺股上漲,雞犬昇天,所以任何人投資績效都是正的,但不帶表大家都是投資高手, 所以就算績效是正的,還要看績效的排名才行!

所以論文中的作者,針對所有參數產生出來績效,從「樣本內」找到「最佳參數」,並且將「樣本外的績效」由小到大排名,並且觀察「最佳參數」是否位於前 1/2 的機率

2. 產生多重的 IS 跟 OOS

假如只有一組 IS 跟 OOS 實再是太少了,這樣驗證的樣本會不足, 所以作者不使用「歷史數據」來區分 IS 跟 OOS,而是將回測整個跑完後,將績效的時間序列切成 S 份,任選 S/2 份當作 IS,其他當作 OOS,這樣的話可以產生超多種組合

實驗結果:

下圖就是作者使用此方法的實驗範例, x 代表「最佳參數」在「樣本外」的效果,越右邊代表效果越好,而 y 軸是樣本的數量 我們會發現,下圖這個例子,整個分佈偏向左邊,也就是 x 軸小於零,代表其實最佳化後,大部分的績效都是比較差的! 每100個樣本,就有74個樣本,最佳化後的效果小於績效的中位數, 所以 74% 的機率,最佳化後的效果比較差

bad strategy 1

一個比較好的策略,應該會是如下圖,整個 每100個樣本,只有4個樣本,最佳化後的效果小於績效的中位數, 所以只有 4% 的機率,有過擬合的風險, 算是一個很不錯的參數優化過程

good strategy

結果我上網找了一下,都沒有 python 的程式碼 所以假如大家有興趣,我之後可以提供這篇 paper 的程式碼, 雖然但我不確定大家有沒有興趣就是了XDDD

多空操作術 7年級生股市提款5千萬

一個大學時期就讀生命科學相關科系、從來沒修過財經課的7年級生,靠著自修理財,打破22K低薪魔咒,5年內在股市提款逾5,000萬元,光是2017年已經賺了3,300萬元。

他的名字叫菲比斯(化名),今年33歲,是PTT股票板上的名人,被網友封為「菲神」,年年在板上公佈對帳單,對帳單上的資產年年成長,讓網友驚呼,更因為進出數據白紙黑字毫無遮掩,所以在PTT股票板上享有高正面人氣。

菲比斯2008年退伍時,遇上金融海嘯。當時失業率攀高、房價居高不下,低薪的22K成為常態,7年級生甚至被笑稱為「三無世代」—無房子、無工作、無婚姻。面對「三無世代」,菲比斯非但沒有默默地接受,反而立志找到更大的出路。他不斷地嘗試再嘗試,終於找到能賴以為生的技能。

菲比斯說:「我是靠著多元策略配置,才讓資產穩穩成長。」近5年來,他三分之二的獲利來自於多空操作術,也就是做多成長股,同時做空衰退股,再用個股期貨等槓桿工具來放大獲利。

初入職場 一退伍就遇到金融海嘯 正職難找靠打工勉強度日 菲比斯原本想從事生技研究的工作,但是,大學入學後發現夢想與現實差異極大,「懷抱夢想念了3年後發現,根本只是『南柯一夢』!」菲比斯說。受到臺灣生技產業環境的影響,大部分的同學畢業後都跑去藥廠當業務,而不是他心目中的專業研究員,因此,他又重拾從小想當個舞臺劇演員、音樂家的夢想,主動延畢一年,準備音樂研究所考試,不過,最後也是無疾而終。

菲比斯退伍時剛好遇上金融海嘯,衝擊全球經濟,他的第一份工作是去速食店賣早餐,「值早班薪水比較高,1小時有140元,每天5點上班,工作到9點,但是後面累到每天早上都快爬不起來了。」菲比斯表示。

基本面3指標挑好股 再看籌碼研判上升力道

菲比斯靠著一籃子股票做多,再加上一籃子股票做空的多空操作術,成為年年獲利的主要核心,他認為,這是一般投資人也能自學的獲利模式。散戶不用怨嘆沒有內線,只要勤看財報等基本面數據,也能像他一樣從股市提款。

「很多人花錢上技術分析的課程,但是卻賺不到大錢,原因就在於技術線型只是股價漲跌其中一個判斷依據,並不是全部。」菲比斯說。長期來看,股價一定會反映公司的獲利。雖然會漲的公司不一定有扎實的財報數據,但是,先確認基本面佳,再用技術面、籌碼面輔助判斷,就能避免買到地雷股。

菲比斯是用什麼指標判斷基本面呢?又是用什麼指標判斷股票是否值得納入做多的一籃子投資組閤中呢?以下是菲比斯挑選做多股票的步驟:

img

步驟1》觀察營收、毛利率、營業利益率變化 證交所規定,上市櫃公司每月10日前要公佈前一個月的營收,因此,菲比斯會在每月10日前後,逐一查閱臺股1,600多檔公司的數據,如果是營收由衰退轉為成長,或逐月成長的股票,他就會進一步檢視該公司最新一季財報的毛利率與營業利益率,是否也較去年同期成長。

上市櫃公司每個月10日以前,必須公告前一個月的營收數據。另外,在每年3月31日之前,必須公佈前一個年度的年報,而第1季~第3季季報的公佈期限,分別為每年的5月15 日、8月14日與11月14日。

營收是一家公司最即時的財報數據,可以作為第一步的篩選,但是,如果營收成長是靠殺價競爭或合併而來,就未必能帶動實際獲利的成長。

因此,除了營收成長之外,最好同時看財報中與獲利相關的2大數據-毛利率與營業利益率,如果2個指標都同時成長,就可以優先列入候選名單。

做空》無懼大盤驟然轉向、加速財富累積

高本益比股獲利停滯 趁評價下修賺波段財

菲比斯透過財報數據選擇做多股的方法,乍看並不難,但是,從財報選股的操作方法,通常給人獲利速度慢的印象,為什麼他的獲利卻這麼快呢?菲比斯說,有2個關鍵的差別,「其他透過財報選股的投資人,通常不會做空,也不會使用槓桿工具。」

菲比斯受到舅舅的影響,除了挑選財報成長的股票做多之外,同時也會挑選財報衰退的股票做空,而且,做多與做空的資金配置會各佔一半,不會特別偏重任何一邊。

多空資金配置宜各半 佈局檔數不要差距太大 投資人普遍覺得,多頭的時候應該盡量做多、空頭的時候應該盡量做空,順勢而為才會賺更多。菲比斯的父親就是以做多為主,不但聽明牌、重壓單一個股,而且又不懂得避險,導致最後賠光退休金。但是,菲比斯的舅舅同時做空與做多,卻是年年都賺錢。因此,菲比斯體會到,股市瞬息萬變、漲漲跌跌,「同時做多與做空,才可以安心睡好覺。」不僅如此,長期來說,兩邊同時佈局,獲利反而更穩定。

投資新手剛開始進入股市時,可能資金不足,只能買1檔股票,的確無法進行資金配置。不過,菲比斯建議,如果有30萬元至50萬元的本金,就可以開始進行資金配置,選擇股價20元~30元的股票,融資做多,再加上融券做空,已經可以同時操作多檔股票了。另外,如果投資人懂得運用股票期貨這種槓桿工具,更能擴大布局範圍(股票期貨的操作方式,請詳見《Smart智富》月刊的第132頁)。

菲比斯提醒,做多與做多的資金比重,不只市值要接近,兩者的檔數最好也不要差太多。一般來說,做空可以挑選的標的檔數,一定比做多的少。假設同樣佈局市值50萬元的股票,做多股票的檔數可能是5檔,做空的可能只有3檔。雖然做空股票的檔數會比較少,但是,千萬不要集中在1檔,仍然要盡量分散。

img

利用個股期貨3優勢 小資金滾出高報酬

「如果沒有股票期貨,我沒辦法賺這麼多、賺這麼快!」菲比斯說。挑選股票是基本功夫,很多投資人都做得到,但是,這對菲比斯來說僅僅是80分的水準,要從80分進步到100分,需要很大的努力,其中一個很重要的關鍵,就是了解衍生性金融商品。

很多投資人一聽到「期貨」、「槓桿」這些字眼就心生害怕,菲比斯認為:「只要瞭解工具,就知道工具是中性,它不是導致你虧損的原因,會讓你虧損的原因是,選錯股又不懂得停損,或你根本不知道這些工具要怎麼用。」

菲比斯說,如果懂得選股,又對衍生性金融商品有足夠的認識,就能把這些高槓桿的工具,變成加速獲利的助力,而不是阻力。菲比斯透過多與空約略各半的資產配置,來分散市場風險,再透過槓桿工具放大獲利,「這是我提高報酬的核心祕密。」

每日結算權益數 免賣出就能拿到獲利金額 股票期貨就是以股票為標的所發行的期貨商品,股票漲,股票期貨就會跟著漲;股票跌,股票期貨就會跟著跌。由於股票期貨的跳動單位與股票一樣,因此,股票期貨的操作邏輯很簡單,就是「看漲買、看跌賣」。

菲比斯最愛的衍生性金融商品是股票期貨,因為它有3個優勢,可以快速讓獲利放大,最明顯的例子是菲比斯做空網家(8044)。

優勢1》槓桿大 菲比斯在網家現股164元時,做空1口網家的股票期貨(1口股票期貨等於2張現股),他不需要準備16萬4,000元(不考慮交易成本),只需要準備4萬4,280元的保證金,一直等到股價跌到119.5元時出場,價差44.5元,賺到8萬9,000元,報酬率高達200%。

如果菲比斯使用融券做空的話,需要準備9成的資金,做空2張網家現股,就得拿出29萬5,200元,假設一樣賺到價差44.5元,獲利8萬9,000元,換算報酬率只剩下30.15%(詳見表1)。

img

資金控管才是王道(一):顛覆你認知的期望值!

https://www.bituzi.com/2013/11/positionsizingexpectation.html

本週我們開始介紹資金控管經典書籍:Van Tharp's Definitive Guide To Position Sizing(How to Evaluate Your System and Use Position Sizing to Meet Your Objectives?)

作者是Van K. Tharp博士。Tharp 2006年還有一本很有名的書: Trade Your Way to Financial Freedom。隨後出了中文本:交易.創造自己的聖盃。(相信很多朋友喜歡這書名的關鍵字: 聖盃!)

最近出的書籍為: Trading Beyond the Matrix: The Red Pills for Traders and Investors.

第一章:黃金交易法則(The Golden Rules of Trading)

\1. 當建立一個新部位時,設好你的初始風險 (Initial Risk, R)。R表示你在初始停損點所遭受的損失。

\2. 用R定義你的虧損與獲利(R-multiples):例如在這交易策略裡,我可以虧損2R,可以獲利5R

\3. 不管做任何策略改變,只能限制你的虧損為1R或是更少。

\4. 確認你策略的平均獲利大於1R

**上面這四條規則有一個最經典的詮釋:降低你的虧損,並且讓獲利飛!(**Cut your losses short and let your profit run.)

範例:你計劃買進Apple股票100股,買到的價位在500元,停損設在跌價5元,也就是495元。

這時,你設定你的初始風險(Initial Risk, R)為5*100=500元。也就是R=500。

在你的交易策略裡,你可能賺取的利潤、損失我們都用R去表示。你只能輸1R或是更少。而必須讓你的獲利大於1R

R是你制定策略所規劃的。然而,可能天不從人願,隔天開盤Apple直接跳空跌了10元,來到490元。

這時,你的損失是10*100=1000元,也就是2R。(這是我們所要避免的,損失就是要控制在1R1R內。)

你當然也可能很幸運,過了一個月後,蘋果漲到600元。你獲利100*100=100,00元。這時你的獲利為20R

上面的計算,沒有包括手續費。當然手續費在實際交易上一定會有的,需要納入考量。因此跳空的損失實際上會大於2R一些(加上手續費),而獲利會比20R小一些(扣掉手續費)

我們假設你有個交易策略,執行下來的交易紀錄如下:

2013年11月01日:IBM股票賺14元 2013年11月02日:IBM股票賠6元 2013年11月03日:IBM股票賠4元 2013年11月04日:IBM股票賺8元 2013年11月05日:IBM股票賺20元 2013年11月06日:IBM股票賠5元

紅色代表賺錢,綠色代表虧損,以此例來說,

平均獲利為14元,平均勝率為50%; 平均虧損為5元,平均輸率為50%;

基本上,只要你的交易策略期望值是正的,就是有可能會獲利的交易策略。

期望值 = (平均獲利勝率) - (平均虧損輸率)

所以期望值為 1450% - 550% = 4.5。這代表著平均每交易一次,可以獲利4.5元。

上面是一般人認知的期望值算法。然而,這4.5元有什麼意義? 

如果今天你的資產只有100元,而每次交易可賺4.5元,那很厲害,資產是以45%的速度成長。

但如果今天你的資產有100萬元,每次交易只賺4.5元,你可能會嫌賺太慢,甚至根本不想繼續這樣的交易。

因此,平均每次交易可賺多少錢,並沒有多大意義! ** ****我們更關心的是:平均每次交易,**每一塊錢平均可以賺多少錢?

以上面的例子來說,若我們將R設為4,也就是每次交易虧損最多不會超過1R = 4元。

則每次交易,每一塊錢可能虧損下,平均可賺 4.5元/4元(可能的虧損) = 1.125(元/可能虧1元的風險)。

換句話說,每次交易,在承受虧損1元的風險下,1塊錢可能會輸光,也可能會變成2.125元。賺了112.5%。從這個角度看,這個交易策略實在表現太好了!

事實上Tharp在第一版的書籍裡也用上述的期望值公式,在第二版才作修正如下,將原來的期望值公式,再除上每次交易可能遭受的風險。

(平均獲利*勝率) - (平均虧損*輸率)

期望值 = ---------------------------------------------------

1R

如果你不知道如何計算你的初始風險R?

大部份的投資人都不知道自己的初始風險是什麼!甚至交易時只是憑感覺買賣,毫無任何交易策略可言。

好一點的投資人會制定交易策略,但還是不知道如何制定自己的初始風險。

Tharp建議,你可將交易紀錄的平均虧損,設為你的1R。

以上述例子而言,平均虧損為5元,因此可將R設為5,代表著每次交易若是遇到虧損,平均會虧5元。

這時你的每次交易,每一塊錢可能虧損下,平均可賺4.5元/5元 = 0.9 (元/可能虧1元的風險)。投資1元若能變成1.9元,真太屌了!

星期五;一天一錠,效果一定,歡迎訂閱「幣圖誌Bituzi電子報」

交易一定有風險,謹慎理財,資金控管才是王道。

資金控管才是王道(二):用SQN衡量你的交易系統!

本週我們介紹 Van Tharp's Definitive Guide to Position Sizing -- Chapter 3 ** **投顧老師:『老師在講你有沒有在聽?只要加入我會員,好的老師帶你上天堂,壞的老師帶你住套房!』

** **牧清華:『別再相信沒有根據的說法了。要說你的交易策略有多屌,我們用科學化的方式衡量交易系統。』

下面六個交易策略,你覺得哪一個最好?(R-Multiple請見上週文章)

"菜籃族"最喜歡的策略特性:每次都贏,贏多贏少無所謂,只要贏就好。

這類型的投資人,喜歡用勝率去做排名,如下:

菜籃族會認為交易策略3最好,玩十次贏九次。可惜,輸的那一次把過去贏的九次都輸回去,還倒賠1R。

如果你有國中數學程度,你是聰明一點的投資人,你可能會用期望獲利來決定哪個交易策略最好:

哇! 交易策略5竄升到第一名,之前勝率最高的交易策略3反而是最後一名。可見勝率高不一定好,勝率低也不一定差。

用期望獲利做排名,順序幾乎與用勝率做排名顛倒。

交易策略5的期望值最高,這意味著,用某種方式做交易,策略5可能可以賺到最多的$$。

然而,市場老手會說,管你期望獲利多少,讓我實際上賺最多$$最重要!

老手Care的是到底賺了多少錢,他們用 期望獲利*交易次數 去做排名:

用實際賺多少的排名,跟用期望獲利排名順序差不多。交易策略5仍然是最好的選擇。

如果你將1%當做你的初始虧損(-1R),策略5跟策略6可以讓你賺到將近61%與36%的利潤。

另外,有些人會把風險擺在第一位的!

評斷一個交易策略的好壞,以最沒有風險的策略為優先考量。他會根據平均虧損來做排名:

以上幾種交易策略評比的追隨者,牧清華都祝你賺大錢。

Tharp採用統計裡面的T-Score來形容 "交易策略有多好"!

Tharp把它訂為 "系統品質指數",公式如下:

System Quality Number (SQN): (期望獲利/標準差)*交易次數開根號

SQN公式有什麼意義呢?很簡單,稍微瞇著眼睛觀察一下不難理解:

\1. 期望獲利越高,SQN值越大。(線性成長)

\2. 標準差越小,SQN值越大。(倒數關係)

\3. 交易次數(N)越多,SQN值越大。(開根號後線性)

以上第1點與第2點皆很合理:

第1點期望獲利越高,本來就該給這系統比較高的分數。

第2點標準差越小,代表交易產生的風險越小,SQN分數自然就越高。

第3點可能讀者比較難理解:

交易次數(N)越多,代表樣本數越多。換句話說,期望獲利與標準差這兩個值得可信度也越高。

自然,可信度越高,代表估的越準確,SQN分數自然就高。

你或許會想,那我就多弄幾次交易,拉高SQN分數。

所以為什麼N要開根號,交易10次和100次的開根號,多交易了90次,SQN分數會差到10倍;但是交易100次和190次的開根號,一樣多交易90次,SQN分數只差了1.37倍。

我們用SQN來看上面六個策略的排名:

然而,若是交易次數過多,確實也會讓SQN失真,例如你交易了10000次,則根號N=100,這會造成SQN值異常的大。

所以Tharp建議,乾脆都用100次就好。最公平!也就是SQN的比較就用"期望獲利/標準差"就好。

如果上面這五個交易策略採用固定交易100次,則SQN排名如下:

我們觀察到,交易策略5又回到第一名。

衡量你的交易策略

當你使用SQN時,分數要到多高才叫做好的交易策略,Tharp給了下面的標準去判斷。

注意到上述六個交易策略,沒有一個分數到達 3以上 (還不錯, Good System)。

Tharp說,如果上面六個交易策略,會有讓你想要拿來使用進場的衝動,那你選擇策略的標準太低!

這大概就是為什麼大多數人交易會賠錢的原因吧!把一個沒那麼好的交易策略當做聖杯在使用。

星期五;一天一錠,效果一定,歡迎訂閱「幣圖誌Bituzi電子報」

當交易次數N太少,SQN也會相對低。Tharp給了以下的建議:

  1. 如果你這策略的交易次數只有十次(N=10),則此策略的SQN至少要大於3.5。
  2. 如果你這策略的交易次數只有二十次(N=20),則此策略的SQN至少要大於3.0。
  3. 如果你這策略的交易次數只有三十次(N=30),則此策略的SQN至少要大於2.5。

資金控管才是王道(三):英雄慎選戰場!

良禽擇木而棲,良臣擇主而事。投資朋友有沒有想過,是否存在一種"萬能的交易系統",適合用在各種市場,適合用在各種盤勢?

牧清華也不知道有沒有這樣的交易系統,但如果你手上有,請偷偷告訴

本週我們介紹 Van Tharp's Definitive Guide to Position Sizing -- Chapter 4

你研發了一套交易系統,回測後產生的交易結果如下:

  1. 39%的勝率 (平均每交易100次有39次是賺錢的)

  2. 期望獲利為2.34R

  3. 在趨勢市場的SQN=4.82

  4. 歷年的最大虧損為-61R,平均每年的最大虧損為-32R

如果上述幾個數據一樣適用在未來的盤勢,那你該如何使用這個交易系統,使得它可以獲取最大利潤?

上面是個很大的問題,正是Tharp這本書要談的Position Sizing。但在談如何操作之前,我們先考慮幾個問題。

  1. 採用的資料是否有代表性? 

  2. 如果過去資料真的有代表性,那這交易策略在未來真能如預期賺錢嗎? 

  3. 你的交易系統適合怎樣的市場?股票?外匯?期貨?商品?

當然不只以上三個問題,但我們今天重點focus在,你的投資策略適合怎樣的市場?

假設今天你的交易系統回測1996年到1999年的NASDAQ,賺了很多錢,績效很好。

可是,那段期間是美股的大多頭阿!如果你今天回測的時間是2000年到2002年,那還是一樣有穩定的獲利績效嗎?

Tharp根據趨勢與波動,將市場分為可能的幾種類型:

市場趨勢可能往上(go up)、盤整(sideways)、往下(go down)

市場波動分為平靜 (quiet)、常態(normal)、不穩定(volatile)

三種市場趨勢,搭配三種市場波動程度,最多共九種類型。那如何區分市場屬於哪種類型呢?

市場趨勢很好判斷,我們若以週為單位,13週為一期,一個最簡單的方法是去計算期末價位相對於期初價位,是漲是跌(up or down),或是差不多(sideways)?

當然,你也可以設計不同的定義,例如50日平均線大於100日平均線 (50ma > 100ma)叫趨勢往上;反之,50日平均線小於100日平均線 (50ma < 100ma)叫趨勢往下,...等。

而要計算市場波動,我們一樣以週為單位,每週波動程度定義為:

週波動率 = (週高點 - 週低點) / 週收盤價

統計S&P 500指數,從1995年1月到2005年12月,市場波動率平均為3.3% (每週);標準差為1.85%。

舉例來說,如果只考慮平靜(quiet)與不穩定(volatile)市場,Tharp定義如下:

平靜市場:在13週內(一期),市場至少有七週波動度皆在3.3%之下。

不穩定市場:在13週內(一期),市場至少有八週波動度都在3.3%之上。

我們也可考慮再複雜點的市場波動,分為平靜 (quiet)、常態(normal)、不穩定(volatile)

常態市場:(期望值+一個標準差) or (期望值-一個標準差) 平靜市場:(期望值+一個標準差之下) and (期望值-一個標準差之上) 不穩定市場:(期望值+一個標準差之上) or (期望值-一個標準差之下)

Tharp整理了1995年到2005年的S&P 500指數的市場分類(以13週為一期,計算市場趨勢與波動率)。

小結: 在1995年到2005年的S&P 500指數,市場趨勢往下時,比市場趨勢往上時更容易有波動。 (說明:趨勢往下的市場裡,有5/7是不穩定的市場,反之在趨勢往上的市場裡,只有4/22是不穩定的市場)

我們拿某個交易策略X回測市場分類後的SQN,整裡如下:

此交易策略X的SQN雖然只有1.94,但可觀察到,策略X對趨勢往上或是往下的市場是具有相當高的SQN值,分別達到3.03與3.05。

此策略用在平靜市場(波動度小)也是非常有效果的,SQN值達到4.32與4.45。

而此策略最怕遇到盤整且不穩定的市場,SQN值竟然是負的。

由以上觀察可知,雖然這是一個SQN值只有1.94的交易系統,可是卻可能是相當實用的。

如果你會判斷何時為趨勢盤,何時為盤整盤,何時為波動盤,何時風平浪靜?那恭喜你,此策略非常適合用在趨勢市場或是波動大的市場,SQN值皆有3以上。

最後總結Chapter 4如下:

關於交易系統,我該瞭解什麼?

  1. 在每次交易前,你必須事先決定最糟情況發生的停損點,並把這情況發生的損失當作你的初始虧損R。

  2. 你的交易結果有賺有賠,但可以表示為一連串R 與R-multiples的比值。

  3. 所有交易系統都可用R-multiples的分佈去表示,包含此交易系統的獲利期望值與標準差皆可預估出來。

  4. 計算此交易系統的SQN值 (期望獲利/標準差)*(交易次數開根號)。當然,你希望交易系統具有高的SQN值,且資料具有代表性。

  5. 判斷你的交易系統適合哪一種市場型態。(趨勢盤、盤整盤、波動、平靜)

  6. 開始尋找更多的樣本(sample)測試,什麼樣的市場,什麼樣的時間點適合使用此策略?

  7. 開始模擬交易策略績效。有多少時間比例會賺錢?最大虧損是多少?平均虧損是多少?預期獲利是多少?賺賠比是多少?

恭喜你可以開始設計你的資金控管 (position sizing)計劃啦!

星期五;一天一錠,效果一定,歡迎訂閱「幣圖誌Bituzi電子報」

再好的戰士,也不可能任何戰役都戰無不勝 -- 拿破崙就不適合滑鐵盧。

真正的戰神,懂的選擇戰場,懂得攻擊時間,懂得收兵時刻。

再好的交易策略,也不可能在任何市場、任何時間,都能夠穩定獲利。

偉大的交易員,懂的選擇市場,懂得出手時機,懂的獲利了結,懂得停損出場。

00713/00675L正二蜈蚣策略

本篇是寫給會看景氣燈號、願意承擔風險、願意擇時、試圖贏過市場報酬的槓桿投資者。

https://i.imgur.com/cutJ45G.jpeg

img

如果你只是想穩定賺取市場報酬,不想那麼複雜,請搜尋「買借死」。 如果你是指數投資基本教義派,請別憤怒,投資金律寫的東西並沒有過時,你可以繼續抱好真理,悲觀的人正確,樂觀的人賺錢。

https://i.imgur.com/zMQoCuQ.jpeg

img

如果你是股債配置追求者,或是害怕臺海戰爭等級的黑天鵝…

img

由於不是新手指南,我也不會回可不可以換成別的高股息、為什麼不買元大正二等問題,請翻一下相關討論。 好了,免責聲明結束。正文開始,請你…

https://i.imgur.com/hRhBKCO.jpeg

img

首先我們先來看整個投資架構 (感謝joe2比對圖)

https://i.imgur.com/oBpsnNL.jpeg

img

https://i.imgur.com/efeo0IP.jpeg

img

假設起點在景氣谷底,即2022年,當下你有一筆錢,要怎麼開始這個策略呢?

1.當下立刻歐印買入00713,保留配息。

2.景氣由藍轉黃藍時,即2023年9月,開上你所有的槓桿。質押、配息投入、信貸,你能承受的情況下大膽歐印00675L。

3.景氣上行過程用配息還質押利息外,持續買入00713,信貸請你用工資還。

4.當進入景氣紅燈時,等待第一個大修正,6-10%的跌幅,並靜待反彈。

5.反彈達前高並再漲3-5%就是出場的時候,看個人風險/貪婪耐受度。賣出00675L,將00675L的錢還質押,多的全額買入00713。

6.配息持續買入00713,靜待熊市。

7.熊市來臨,停止買入00713,配息拿來還信貸、多的存起來。

8.回到1,loop。

整個過程的流程,就是:底部留現金,上行開始拉滿槓桿,頂部降倍率槓桿並持續買入,下行停止買入,降資金槓桿,靜待復甦。

之所以要追高,是由於00713的目的是為了購入00675L,當上行時,吸收到的市場報酬穩定流入00713的池子,而當下跌時,00713抗跌的特性能等比例放大購入00675L的能力。

假設現在紅燈。 我有100萬的00713,可質押60萬。 假設00675L一張10萬,共可買6張。

當股市跌回藍燈,大盤修正25%。 00713約跌20%,剩下80萬。 00675L跌50%,每張價格剩5萬。

80萬的00713能質押48萬。 48萬能購入9.6張00675L。

得證,上升期買貴是假議題。市場報酬在下跌時,保留並等比例放大,購買的張數更多。沒必要躲崩,全程待在市場並調整槓桿更好。躲崩的習慣讓你容易誤判,錯過大漲。

寧可接受帳面損失。帳面損失不是損失,你購買的標的是00675L,不是現金。

00713的特性是吃魚頭策略,持股起漲就換股,這保障他下跌時不至於重傷,而00675L是用來補足在上升期00713報酬不足的缺憾。

因此只要有多的錢,景氣上升期不用管價格,閉眼買00713就對了,吸收到的市場報酬將是你下一輪景氣循環歐印00675L的本錢。

投資要有耐心,不急一時的結果,持續待在市場,相信你會安度每一個危機並找到屬於自己的黃金國。

====

你可能會問:如果我現在想入場,該怎麼做呢?

由於現在走到3,為了風險管理,不該槓桿全開,因此比較好的作法是:

1.明天歐印00713,並質押30%,買入00675L。

2.等中期修正(也許是9月中)再投入剩下30%質押額度。不要信貸。

3.配息繼續買入00713。

4.銜接流程3-4。

這是比較適妥的風險平衡,激進一點可以60%買滿,看耐受度。

只要記得紅燈降槓桿即可。

討論前建議翻一下熊市那篇,許多問題那邊都有回答喔!

Q1 碰到藍燈盤整怎麼辦

https://i.imgur.com/bhfFnfE.jpeg

img

質押00713+正二台股大跌帳面虧損49萬...達人「照樣買進」:遲早會翻紅

img

img

img

img

至於還處在虧損狀態的00713,我是完全不擔心它。我相信多給它一點時間,它的帳面遲早會翻正。

我對它有信心,不然也不會把它買到88張。至於這次00713的表現就不用多說了,本次大盤最大跌幅達19.47%,00713最大跌幅為8.68%,甚至不到大盤的一半。

00713在2020、2022年股災表現都很穩,這次大跌依舊展現它的抗跌能力。

可能很多人認為我買正二又質押,這次肯定會很慘。但其實我在6月19日就說了,我現在的00713/正二比例為7:3。我的操作方式其實偏保守。

再加上我質押的是00713,所以這次整戶維持率最低都還有183.43%。

此外,我手上還有4張正二,若整戶維持率持續下跌,都可以把這4張正二用質押不借款的方式,提高整戶維持率。

不過後續股市反彈上漲,所以我的4張正二就讓他們出去借券賺零用錢羅。

這次大跌,其實是讓我們都有一個反思的機會。

開槓過大的人經過這次大跌,就能明顯感受到壓力。曝險過高的人,可能也會承受不住龐大的心理折磨。

不過其實這些都不用苛責自己,畢竟誰不是從經驗中學習成長?遇到了,就慢慢調整就好了,沒什麼大不了的。

公開基本面選股4心法 5年從股市提款5000萬!

從多方面指標挑選標的 才能降低買到地雷股機率 「很多人花錢上技術分析的課程,但是卻賺不到大錢,原因就在於技術線型只是股價漲跌其中一個判斷依據,並不是全部。」菲比斯說。長期來看,股價一定會反映公司的獲利。雖然會漲的公司不一定有扎實的財報數據,但是先確認基本面佳,再用技術面、籌碼面輔助判斷,就能避免買到地雷股。

菲比斯是用什麼指標判斷基本面呢?又是用什麼指標判斷股票是否值得納入做多的一籃子投資組閤中呢?以下是菲比斯挑選做多股票的步驟:

img

操作心法①觀察營收、毛利率、營業利益率變化 證交所規定,上市櫃公司每月10日前要公佈前一個月的營收,因此,菲比斯會在每月10日前後,逐一查閱臺股1,600多檔公司的數據,如果是營收由衰退轉為成長,或逐月成長的股票,他就會進一步檢視該公司最新一季財報的毛利率與營業利益率,是否也較去年同期成長。

營收是一家公司最即時的財報數據,可以作為第一步的篩選,但是,如果營收成長是靠殺價競爭或合併而來,就未必能帶動實際獲利的成長。

因此,除了營收成長之外,最好同時看財報中與獲利相關的2大數據—毛利率與營業利益率,如果2個指標都同時成長,就可以優先列入候選名單。

操作心法②查詢近期新聞或法人報告確認趨勢 看到營收、毛利率與營業利益率成長,但是,財報畢竟是反映企業營運過去的數字,未來趨勢是否繼續,則要透過質化的分析。

要了解公司未來的營運展望,該怎麼辦呢?菲比斯會查詢近期新聞、閱讀法人的研究報告,如果有相關原物料或成品的報價,則可以同時看價格的變化。例如:投資太陽能公司,就一定要到市調公司集邦科技的網站,確認現在太陽能報價是否也同步向上。

當產業報價向上,而公司營收與財報獲利都成長,就代表公司處於順風期,做多的獲利機會很高。但是,當產業報價開始下跌,就要留意公司未來的趨勢是否向下,因為公司發展會受到整個產業景氣影響,菲比斯說:「要提高投資期望值,就要勤勞研究。」

操作心法③檢視外資籌碼是否連3個交易日增加 在臺股逾1,600檔的股票中,不乏有基本面很好,但是股價卻不受青睞的公司,菲比斯會再搭配籌碼面,協助判斷股價近期是否會上漲。

籌碼是指誰拿錢出來買,菲比斯最愛看的籌碼指標是「外資」。外資買進1檔股票通常都有延續性,今天買明天再買的可能性很高。一旦外資持續買進,不但代表外資看好這檔股票,更代表外資本身就是推動股價上漲的力道。菲比斯偏好選擇外資連買至少3個交易日的股票,股價未來繼續上漲的可能性較高。

操作心法④用本益比與殖利率估算股價是否昂貴來找買點 菲比斯評估一家公司目前股價是否值得投資,有兩個指標:一個是本益比、一個是殖利率。 1.本益比→低於10倍算便宜 本益比是最常用來評估股價昂貴與否的指標,法人也都用它來評估股票的目標價。

img

一般來說,本益比愈低愈好,代表股價愈便宜,買進後可以預期的報酬率空間愈大,但是,只看本益比低就買進,有可能挑到衰退的公司,股價低還有更低,因此,菲比斯提醒,要按照步驟先挑出有獲利且趨勢向上的公司,再來計算公司的本益比。

另外,不同產業,市場給予的本益比也不同。例如:工業電腦類股的本益比約在10倍左右,但是,IC設計類股就享有很高的本益比,而龍頭股通常也比同產業的其他股票,享有更高的本益比。

不同產業的本益比標準不同 計算時要用「預估EPS」 當股價還沒上漲,但是,獲利已經成長或預估今年會成長,本益比就會降低,就值得買進。可是,要如何判斷本益比多低才算便宜呢?菲比斯給了個粗略的答案「10倍」!

10倍上下的本益比,不管是哪個產業、無論是否為龍頭股,都算是非常的低。菲比斯建議,有興趣的投資人,可以自己估算每檔的本益比,一季一季不斷計算之後,就能自行判斷出來,不同產業本益比的合理區間在哪個範圍了。

要注意的是,由於股價是反映公司未來的趨勢,因此,菲比斯在計算本益比時,會用「預估」的每股盈餘(EPS),最簡單的方法就是用最新一季的EPS直接乘以4,再用最新營收數據進行調整,但是要留意,有些股票有淡旺季分別,在評估時也要跟著調整。如果產業報價成長快速,評估時也可以偏樂觀些。

2.殖利率→高於4.5%算便宜 殖利率就是投資人買進一檔股票,可以拿到多少的股利回報率。在計算股票殖利率時,一般只計算「現金股利」。

img

中大型股票在法人眼中,殖利率4%已經算不錯,超過4.5%即屬於高殖利率股票,股價就算很便宜了,但是,小型股的殖利率可以提高至6%。菲比斯一樣會用「預估」的方式來計算殖利率,先預估個股今年獲利,再看過去3年該公司的股利配發率。假設今年預估的EPS為10元,過去3年的股利配發率為80%,可以合理期待隔年的現金股利是8元,再除以目前的股價,就是預估殖利率。

月營收公佈前先檢視股價 若漲勢不再就應賣出持股 當買進理由消失,例如:營收下降、毛利率下降、營業利益率下降,或產業報價由持續上漲轉為不斷下跌,或外資持續賣超等,菲比斯就會賣出。

另外,有時候股價會領先基本面,當次月營收還沒有公佈,或還沒有到每季財報公佈日時,股價就已經漲不動了,遇到這種狀況時,菲比斯也會賣出。

img


讓菲比斯在股海找到成功獲利的方式:1.找到一個交易量大的客戶,貢獻穩定的業績,讓菲比斯可以專心操作;2.閱讀了《財報狗教你挖好股穩賺20%》這本書,讓他了解如何利用財報挑出好股票;3.股票期貨流動性變佳,讓他可以用小錢放大投資獲利。

《財報狗教你挖好股穩賺20%》這本書的重點,就是要教投資人,從財報選出好股。這一年,菲比斯開始檢視公司基本面的數據,挑選營收、毛利率都成長,而且本益比不高的個股,建立專屬的一籃子股票來做多,並且透過股票期貨這個槓桿工具來放大獲利。另外,菲比斯也會挑選本益比過高,但是營收與毛利率成長卻跟不上的股票,建立一籃子的股票來做空。

菲比斯的投資資產可以成長這麼快,一定是敢壓、敢衝,「其實菲比斯的操作非常保守、穩健。」菲比斯的第一個主管、也是現任國泰期貨副總經理宋政憲表示,「菲比斯不會重壓、也不會賭一把,他這樣做是對的,因為投資是比氣長,而不是短期獲利。」宋政憲在業界看了太多快速起來,又快速賠光的例子,例如:曾經有位投資人在3個月賺了1億元,但是3天內就賠光了。

菲比斯表示:「我靠著多元策略配置,才讓資產穩穩成長。」近5年來,他三分之二的獲利來自於多空操作術,也就是做多成長股,同時做空衰退股,並且善用槓桿工具來放大獲利,另外三分之一則來自於選擇權當沖。

什麼是菲式思考?

我的思考本身基於邏輯推理與歸納,並推導出結論,由A→B,B→C,C→D,D→E的思路去快速推導成A可能導致E,我會用已知的資料去猜導向的結果,用正確的機率與時間來換取成果,長久習作以後,便成了投資的直覺反應。

而隨著資產增加,在風險趨避原則下,我選擇用橫向分散的方式增加猜題機會、不過分壓重注來降低重注的流動性風險與可能導致的心理風險,並且雙向尋找標的以降低市場波動。

也許許多人都先入為主地認定,能達成高績效的,必定為積極型投資人。但依我對自身的瞭解,我屬於保守型投資人,只是相對懂得善用工具。

整本書貫徹的就是Phoebus Thinking,也就是我的想法;而Pho Thinking發音近似For Thinking,希望讀者也能在閱讀的過程中思考。

相較於技術分析或其他投資相關事宜,我認為我的思考方式才是真正讓自己在這條路上穩定前進的關鍵力量。

我專職投資,但如果你能把我描述的想法與精力轉化到自身的工作或創業項目上,我相信也能幫助你取得更佳的成果,期許你的精神領域也能因本書受惠。

如何正確出場及轉念不凹單?

會投資一家公司,必定有根據的理由,當這個理由消失,就是出場的最佳時機。

以我來說,通常看到一家公司業績轉好,或該產業需求大於供給,身為一個聰明的股東,看的是可見而不遠的未來,如果看得還不是那麼清楚,那麼營收就是檢核業績最具體的方式。

臺灣市場每個月初皆會發布上個月的營收,因此跟不會如此發布營收的市場相較,我們有更透明的管道可以得知更為即時的營業狀況。

換句話說,如果營收成長的趨勢告終,或者公佈的營收與公司法說會訴說的展望不符,那麼我們就視為該公司的表現相對於自身所希望的有所落差,也許不見得全面掉單,僅是需求延後,但是不論展望轉差或需求延後,終歸一句不符預期,而這個不符預期的時刻就是最佳的出場時機。

**許多人常常對股價有固著的想法,認為一定要到達多少的價格才願意賣,可是公司的營運狀況不會因為你多少想賣而有所變化。**在你進場時可能對公司的狀況與股價有所預期,可是就如同地球無時不刻都在公轉自轉一般,世界、產業、公司的狀況,每分每秒都可能有些不同,而這個變化的過程會是逐步產生的,就像戰爭或地震的發生都會改變區域的產業狀況,當轉變來臨時,卻固守進場時的想法,那就是用過去的落後資訊來幹預即時資料的判讀了。

營收的判讀與營運狀況的確認僅是其一,其他包含股權的稀釋、影響籌碼供需變化的因子或公司的重大變革都有可能是我會轉換持股的原因,畢竟投資是為了賺錢,任何提升獲利成長風險的原因與不合理的股權稀釋,都會是一個啟動點,而當那個啟動點發生,我的態度是越早執行越好。

此外,資金有其成本,放著不賺錢,基本上就是虧了通膨,**如果會有凹單的情況,就代表策略已然失敗,而你錯過了真正該出場的那一刻。**此時這筆單便成了雞肋,食之無味、棄之可惜,錯誤已然造成,至於這筆單的去留,則視當時該項投資的展望、價格及是否有更佳的替代選擇而定。

投資額本身代表了你的購買力,你願意花多少的購買力在這個投資項目上。我們控有自己的資金,基本上購買力都是有限的, 不同的選擇彼此間互相排擠,如果你把購買力投入在看不見未來的項目,那何不將資金抽回,轉而投入在相對明朗的已知未來中呢?

當然你可能已經錯過了最佳停損點,不如忘掉你的成本吧!成本根本就不重要,依據市場上現在的價格,並考慮投資項目是否能復甦、需時多久? 如果這個產業被典範轉移,被其他的新東西取代,那它可就回不去了,如果它是民生必需,景氣循環下,終有回來的一天。

但如果這個景氣循環長達十年,你也要把錢放著等它十年嗎?我認為,把錢抽回,8年後再來考慮這個投資項目會是較佳的做法,中間的過程也許會比空等著更有所獲(或更差)。前提是,價格並未顯著低於公司價值,以及你能找到較佳的資金停泊處,誠如我說的,舉頭所望皆有投資機會,能否掌握,仰賴你的細心觀察。如果你不想花心思,機會自然不會屬於你。

每天醒來第一件事就是做投資功課 億級達人曝買股3心法

出處

https://tw.news.yahoo.com/10%E5%B9%B4%E6%BB%BE%E4%B8%8A%E5%84%843-%E6%AF%8F%E5%A4%A9%E9%86%92%E4%BE%86%E7%AC%AC-%E4%BB%B6%E4%BA%8B%E5%B0%B1%E6%98%AF%E5%81%9A%E6%8A%95%E8%B3%87%E5%8A%9F%E8%AA%B2-%E5%84%84%E7%B4%9A%E9%81%94%E4%BA%BA%E6%9B%9D%E8%B2%B7%E8%82%A13%E5%BF%83%E6%B3%95-215857205.html

許瀞文

2023年12月7日

去年底東元股價淨值比不到1倍,菲比斯大舉進場。

去年底東元股價淨值比不到1倍,菲比斯大舉進場。

過去10年,每年都在PTT股版上分享對帳單、資產呈倍數成長,被網友封為「菲神」的菲比斯,2013年從10萬元進場,如今累積億元身價。他說,自己的獲利金三角是從市場供需出發,以基本面選股,再搭配股期、期權槓桿操作;尤其日日寫交易日誌、月月檔檔檢查營收變化的紀律,讓他股海翻身。

img

菲比斯強調,他的投資方式是「看到事件發生再來反應」,所以必須留心推敲生活周遭發生的事件。時至今日,他每天醒來的第一件事是打開財經新聞臺,接著讀報;勤跑公司法說、鑽研財報數據,更是日常生活,像是每月10日公司公佈營收,3、5、8、11月公佈季報及年報,他都會不厭其煩地瀏覽所有個股變化,進而汰弱留強。

心法一、事件操作

菲比斯以近期操作的重電股為例,「近幾年臺灣不斷有跳電情況,去年底新聞報導臺電韌電計畫,10年內要投入5600多億元,看新聞就能知道與該計畫相關公司,包含士電、東元、中興電、亞力等7家公司。」他因此開始仔細閱讀這些公司的財報。

心法二、財報挑股

「讀了財報才發現,東元實在太便宜,股價28元,但每股淨值卻有35元;亞力也是,股價25元,去年第二季財報公佈每股盈餘為0.66元,且10月營收比第二季最高的月份還高,年增率達75%,以第二季每股盈餘乘上4季,推估本益比不到10倍,都是可以買入的安全價位。」菲比斯解釋,財報的眾多數據中,他最在意的就是營收、營益率、毛利率、每股盈餘、本益比及股價淨值比。

其中,他又最重視營收成長及來源,「一家公司業績轉好,或該產業需求大於供給,營收是檢核業績最具體的方式。年增率最好要10%,且每月營收持續增加,因為營收增加,營益率、毛利率也會跟著上升。」

心法三、進出有據

菲比斯透露,經過多年的實戰經驗,他通常以本益比10至15倍、股價淨值比1.8至4.5倍,作為觀察股價依據,像是股價淨值比大於4.5倍就是偏貴價格。至於股價低於淨值,是難得的撿便宜機會,去年底東元股價淨值比甚至不到1倍,菲比斯因此大舉進場。

談到賣股時機,除了股價淨值比,見到營收衰退、公司大股東不正常賣股,菲比斯也會選擇退場、或者反向放空。他舉例,今年4月起亞力營收成長力道不如前幾個月、5月營收還較4月衰退,於是他慢慢賣出、獲利了結,半年時間股價大漲一倍。


「賺錢的關鍵,說穿了就是供需,投資是找出市場的供需面,就能獲利。」每年都在PTT股票版上分享自己的對帳單,資產年年倍數成長,被網友封為「菲神」的菲比斯,為自己10年資產成長超過億元下了一個註解。

2007年菲比斯才剛大學畢業,對自己未來摸不著頭緒,因為一張期貨商徵人的廣告,而踏入投資市場,原本大學就讀生技、對財報一竅不通,靠著自學、下苦功觀察市場,找到自己獲利的方法;之後從10萬元資金開始進入市場,如今投資資產超過億元,「研究公司基本面、個股多空操作、期權避險投機,成為我投資獲利的金三角。」他強調。

菲比斯提到,挑選個股前,應先觀察市場上現在的供給與需求面。以2021年長榮海運貨輪擱淺在蘇伊士運河窄處為例,蘇伊士運河是歐亞之間重要的海運廊道,佔全世界海運量14%,而長榮海運船身長400公尺,難以自立脫困,導致越來越多貨輪擠在蘇伊士灣。

「更多的貨物、更久的航程、較少的通運量,這三因素點燃運價狂噴的引信,這就是供需。」菲比斯解釋,當時長榮股價不過40元,他與身邊好友大力買進,股價200元時全數出場,而他用長榮賺到的獲利,買下現在市價億元的家。

特別的是,當時萬海航運的股價比航運龍頭股長榮整整多了100元,菲比斯指出,這是市場上的股權供需。萬海當時的股本是280.61億元,長榮的股本則是529.08億元,「再次回到供需面,因為萬海的股本小,流通張數也較少,購買力對股價的影響更顯著,也因此股本小的股票跟股本大的股票相比,波動更劇烈。」他解釋。

買權賣權是什麼?選擇權基本運作與策略運用

出處:https://gooptions.cc/%E9%81%B8%E6%93%87%E6%AC%8A%E6%95%99%E5%AD%B8/#%E7%82%BA%E4%BB%80%E9%BA%BC%E8%A6%81%E6%93%8D%E4%BD%9C%E9%81%B8%E6%93%87%E6%AC%8A%EF%BC%9F%E9%80%A3%E8%82%A1%E7%A5%9E%E5%B7%B4%E8%8F%B2%E7%89%B9%E9%83%BD%E5%9C%A8%E7%8E%A9%EF%BC%81

選擇權(英文Options)是一種買賣方可於未來看狀況再決定是否要交易的權利,以權利金進行交易。買方,買進選擇的權利並付出權利金;賣方,賣出選擇權利可收取權利金。買賣兩方在契約約定日期再決定是否執行交易,而這日期就是選擇權結算日。選擇權交易是為了避險而設計,判斷期貨未來價格將上漲可買CALL(買權),判斷會下跌可買PUT(賣權)。

快速瞭解 選擇權 精華重點 key takeaways:

  • 選擇權是一種買賣方可於未來看狀況再決定是否要交易的權利,以權利金進行交易
  • 選擇權優勢:降低持倉成本、看錯不一定賠、小金額對沖高風險
  • 交易立場很重要:賣CALL是看漲不過高但不一定是看空、勝率高
  • 選擇權不建議裸賣,用價差單交易可以有效降低風險,提升勝率
  • 臺股選擇權有『週選擇權』與『月選擇權』,週選擇權每週三結算,月選擇權在每個月第3個星期三進行結算,每月結算一次
  • 同時持有買方、賣方部位的CALL和PUT進行策略組合,可提高交易勝率。例如Iron Condor同時賣出CALL和PUT保持中立

著名投資人Michael Benklifa認為:「交易股票就像是你有一個好工具,而交易選擇權就像是有一整個工具箱。」可見選擇權在金融市場中是十分強大的投資工具!透過本文的選擇權教學,我們將帶你輕鬆入門選擇權投資,瞭解買權賣權的運作方式、看懂選擇權策略圖,打造長期穩定的獲利來源。

為什麼要操作選擇權?連股神巴菲特都在玩!

在本文開始之前,也可以先看影音說明,加深學習印象。

選擇權投資優勢1:降低持倉成本

股神巴菲特(Warren Buffett)其實是選擇權大莊家,從他的公司財報就可以看到,美股選擇權交易佔巴菲特公司營收不小的部分,而他最常用的作法之一就是「賣-賣權(賣put)」,也就是「看不跌破」。

當巴菲特要買入一家公司股票,他的做法是先賣這家公司股票的賣權(Put),假設現在公司股價100元,巴菲特可能會賣履約價60元的Put,收20元權利金。接著巴菲特著手買入該公司的股票。因為大量買入,股價上漲,賣出的put價值降低後,這20元權利金就是實拿獲利。雖說股價是100元,但巴菲特其實只花了100-20=80元買入,賣選擇權同時也幫助巴菲特大幅降低持倉成本。

選擇權投資優勢2:看錯不一定賠

假設大盤在17200,如果判斷臺積電財報數字亮眼、準備上漲,可以賣出臺股選擇權 17000的賣權(賣PUT),當臺積電上漲帶動大盤上漲,賣出的PUT則獲利,降低臺積電現貨持有成本;萬一臺積電未如預期上漲,但是臺股也沒下跌,選擇權結算時大盤點位在賣出的17000PUT之上,這口PUT還是獲利的,可見選擇權策略運用正確,就算看錯方向也不一定會虧損。

認識買權賣權(Call / Put)

買權賣權

  • 買Call (買-買權)⮕ 判斷指數「會漲到」某個點位
  • 買Put (買-賣權)⮕ 判斷指數「會跌到」某個點位
  • 賣Call (賣-買權)⮕ 判斷指數「不會漲到」某個點位
  • 賣Put (賣-賣權)⮕ 判斷指數「不會跌到」某個點位

Call表示買權,Put表示賣權,而在選擇權操作中,所有策略都是由這4個選擇權動作組合而成,以下為你進一步解析:

買Call

判斷指數「會漲到」某個點位,屬於「看多」。例如,現在指數是16000,認為會漲到16200,那就可以買16200Call。

買Put

判斷指數「會跌到」某個點位,屬於「看空」。例如,現在指數是16000,認為會跌到15800,那就可以買15800Put 。

賣Call

判斷指數「**不會漲到」**某個點位。例如,現在指數是16000,認為不會漲到16500,那就可以賣16500Call 。要注意的是,賣Call跟看空、做空是不同的事情,千萬不要覺得賣Call就是看空,賣16500Call就算大盤一天大漲200點,從16100漲到16300,只要沒漲過16500賣Call都是獲利。

賣Put

判斷指數「**不會跌到」**某個點位。例如,現在指數是16000,認為不會跌到15500,那就可以賣15500Put 。同樣地,賣Put 跟看多是不同的事情,千萬不要覺得賣Put 就是看多。我將交易方式、動作意圖和資金列在表格中:

交易判斷資金注意事項
買CALL(買 買權)- 為買方判斷指數「會漲到」某個點位支出權利金,從帳戶內扣款看多、勝率低
買PUT(買 賣權)- 為買方判斷指數「會跌到」某個點位支出權利金,從帳戶內扣款看空、勝率低
賣CALL(賣 買權)- 為賣方判斷指數「漲不到」某個點位收權利金,但帳戶內要有保證金看漲不過高但不一定是看空、勝率高、不建議裸賣
賣PUT(賣 賣權)- 為賣方判斷指數「跌不到」某個點位收權利金,但帳戶內要有保證金看漲不破低,不是看多、勝率高、不建議裸賣

選擇權交易說明

如果想做空或對沖風險,可參考做空教學 用期貨空單取代股票放空來對沖虧損風險的方式


你需要這3個『能幫助交易更穩定』的資訊嗎?

  1. 說明波動率指數影片 x2 (*這些影片我從未發布在任何公開管道
  2. 超優惠開戶管道 x2
  3. 選用選擇權策略的評估過程說明影片 x1

需領取請點此連結


選擇權損平點怎麼看?買賣選擇權策略解析

瞭解操作概念要先從「買賣方式」開始理解。以下為你詳細說明:

買進買權(買CALL)

買Call 很直覺,因為大家都會買東西,只要一張圖片就可以理解。

買進選擇權策略圖

買東西就要花錢,當大盤在16200時,買16400Call支出20點(臺幣$1000元),需瞭解以下重點:

  • 時機:看漲。買Call是看漲,認為大盤會漲到16400,則買入16400Call。
  • 最大虧損:20點。選擇權每週三進行結算,結算時大盤沒漲到16400,則支出的20點權利金歸零,最大虧損為20點。
  • 最大獲利:無限。大盤漲上天買Call就賺上天,但勝率極低就是了。
  • 損平點:16420。

買方損平點為什麼是16420呢?雖然看對方向買CALL是看大盤漲到16400,但大盤到了16400其實還沒開始獲利,因為一開始花20點買CALL,所以需要大盤繼續漲,把支出的20點權利金都漲到了才是真的開始獲利。從上圖中也可以看到,低於16400的紅色區塊是最大虧損20點,中間橘色區塊是「虧損漸減區」,漲到16420過後的綠色區塊才開始獲利。

P.S. 上述提到的『16200、16400』是履約價;『20點』則是一口的權利金。

賣出買權(賣CALL)

知道怎麼買東西,就要知道怎麼賣東西,其實真的很簡單,一樣也是看圖就能理解它的運作機制。

賣出選擇權策略圖

賣16400Call(賣出買權)收20點權利金,賣東西收錢一樣很直覺!同時必須掌握以下重點:

  • 時機:看漲不過。賣Call是看漲不過,認為大盤不會漲到16400則賣出16400Call。
  • 最大獲利:20點。不管大盤從當初建倉時的16200漲、跌、盤整,只要結算時大盤沒漲過16400,收到的20點權利金全拿。
  • 最大虧損:超過16420後無限。無限虧損聽起來好像很可怕,但其實是個很好打破的迷思。只要用價差單進行賣出,除了可以大幅降低保證金需求,還可以完全控制虧損的風險。(歡迎參考:選擇權價差單是什麼?瞭解價差單概念、目的,打破風險迷思!
  • 損平點:16420。

賣出16400Call,表示認為大盤不會漲超過16400,當大盤低於16400都能賺錢,如果漲到16400以上則要開始賠錢給買方,橘色區域是「獲利遞減區」,漲過16420則開始虧損,為什麼是16420才開始虧損?由於賣方是先賣出才收錢,先收了20點,一旦漲超過16400,大盤每漲1點要賠給買方1點,會先從買方那裡收到的20點開始賠,也就是拿買方自己的錢賠給他,若是把收到的錢賠完才會賠到本金。

光是這一點「買方vs賣方」的勝率就差很多了,買方要花錢買機會,還要漲到把花掉的錢賺回來才算是真獲利;賣方則是先收錢,若漲到16400,可以先拿收到的錢賠給對方。

於是我們知道了:

買選擇權要先花錢,賣選擇權可以先收錢

教你看懂選擇權策略圖,掌握賣方損益計算!

裸賣Call損益計算方式

「裸賣」顧名思義就是隻賣Call ,但沒做價差保護。賣一口16400Call ,收到20點權利金。從下圖來看,橫軸是大盤點位,縱軸是損益變化:

裸賣選擇權策略圖

  • 最大收益:當大盤結算在16400以下,穩拿20點。
  • 損平點:當大盤結算在16420整,打平。
  • 虧損:當開始往16420以上繼續漲,那就每1點都要賠錢,漲到16500點就賠80點。

**我建議千萬不要裸賣CALL,不僅獲利有限、風險無限,還要押很多保證金,大盤上漲會持續虧損,很可能血本無歸,必須用價差單策略控制風險。**下方影片教你用這兩個方式取代裸賣。

那麼賣call該怎麼賺錢呢?我們延續剛剛的例子:

  • 狀況1:賣16400的call,當初跟買方收20點權利金,結算後大盤收盤在16300,選擇權價值為0點,賣方需花0點買回來平倉,則獲利20–0=20點。
  • 狀況2:賣16400的call,當初跟買方收20點權利金,結算後大盤收盤在16410,因為大盤漲的比16400call的履約價還要高10點,所以賣方要賠給買方10點。但是當初賣出收的權利金是20點,所以賣方還是獲利,獲利20–10=10點。

裸賣PUT損益計算方式

裸賣Put 卻沒有做價差保護,當你賣一口12200Put 收到20點權利金,該如何計算損平點呢?以下帶你一起看選擇權策略圖:

img裸賣選擇權策略圖

  • 最大收益:當大盤結算在12200以上,穩拿20點。
  • 損平點:當大盤結算在12180整,打平。

為什麼損平點是12180呢?因為賣出Put ,當跌破履約價,每跌1點就要賠給買方1點,跌到12180要賠20點,不過一開始權利金就先收了20點,所以拿權利金去賠給買方。賣出Put 的履約價減掉收到的權利金等於損平點位。

以下提供50秒影片,為你說明賣Put的獲利方式,幫助您更好理解與記憶!

提高勝率的平倉方式教學

選擇權平倉是:賣出選擇權能收權利金,之後花較少的權利金買回來跟原本賣出部位互相沖銷就是平倉。以賣CALL為例說明,有3種狀況CALL權利金都會下降,代表賣方賣CALL可獲利平倉:

  1. 大盤下跌,CALL權利金減少
  2. 大盤盤整,時間價值降低讓外在價值降低,CALL權利金會減少
  3. 波動率降低於是外在價值降低,CALL權利金減少

以上三種狀況賣CALL的賣方都能平倉且獲利。大家都知道獲利平倉,但何時才是適合時機呢?獲利50%時平倉是好時機。

透過數學原理,我在這另一篇文章詳細說明為什麼 選擇權獲利50%時平倉就是好時機!這樣做可以提升勝率,而且這是有理論依據與數學證明的。

臺股選擇權2種合約與選擇權結算

買賣CALL和PUT是選擇權的交易動作,而臺股選擇權有**「雙週選擇權」「月選擇權」**兩種合約供投資人交易:

  • 雙週選擇權每14天結算一次,於每星期三8:45開放新合約交易,直到兩週後的星期三13:25進行結算,所以每個週三都同時有新、舊合約可交易。
  • 月選擇權是在每個月第3個星期三進行結算,每月結算一次。

*注意事項,選擇權結算價是依照大盤點數,也就是加權指數進行計算,不是看臺指期貨大臺、小臺,結算日當天所有買、賣的選擇權合約都要進行結算。臺灣券商會於每月第1週要結算的週選合約用W1表示,第2週用W2,以此類推。但是不會有W3,因為月選合約會在當月第3週的星期三進行結算,所以不會有W3合約。

下方列出2023選擇權結算日期總整理,幫您快速掌握交易時間。更多細節可以參考 選擇權結算日與策略運用 說明。

2023年選擇權結算日日期2023年選擇權結算日日期

選擇權策略運用

除了單純買、賣CALL或PUT的交易,透過同時持有買方、賣方部位的CALL和PUT進行策略組合,可以幫投資人有效避開追漲殺跌看方向操作,進而提高交易勝率。例如Iron Condor(中文:鐵兀鷹)是選擇權最早出現的策略之一,透過同時賣出CALL和PUT保持中立。

常見的選擇權策略與使用時機:

2023年中立策略Iron Condor隆重回歸

2023了,邁入新的一年,今年主軸是開發自動化交易機器人,目前已經完成策略初步回測階段,成效非常好,目前有2個策略回測2020~2022的數據,都有傑出表現。我先說在回測過程中意外發現的事情。

上述說到,2020開始的後疫情時期臺股走勢發生變化,我比較少用IRON CONDOR,但在開發交易機器人的過程意外發現,過去兩年期間,週一週二使用iron condor績效出乎意料好,這邊的好不是幾十上百倍回報,而是固定週一或週二在適當時機與點位做iron condor,穩穩做回測數據是能保持長期穩定獲利。

以前中立策略Iron Condor習慣是週三開始建倉,維持中立把時間價值賺起來,遇到危險則調整部位,沒想過是到接近結算的週一才建倉,完全出乎意料。

知道我說『中立策略Iron Condor隆重回歸』的原因後,我帶你好好認識這個策略,不管是新手或是老手,都必須來瞭解這個選擇權最早被發明出來的策略,學會後,再看3種 iron condor分別如何進場建倉。

Iron Condor的做法是同時賣出CALL也賣出PUT,同時看大盤漲不過高點也跌不破低點。假設大盤在15000,同時賣15200 CALL和14800PUT,接下來,不管是從15000開始的漲跌或盤整,結算時大盤落在這個400點的區間中,你都獲利,這就是維持中立的意思。

當你保持中立,讓大盤在這400點區間中上下走,你將很能好好避開典型散戶的追漲殺跌心態。

3種iron condor建倉方式

中立策略Iron Condor建倉方式總結有3種:

  1. 對稱Iron Condor 先做再說:不管當下狀況是有趨勢還是盤整,直接在大盤往上和往下一樣距離履約價建倉賣出CALL和PUT價差單,把大盤夾在正中間。
  2. 有明顯趨勢時建倉:可透過順勢或逆勢建立出第一邊,接著找相近的delta建立第二邊,做動態Iron Condor。這個狀況主要是用在當下可能有明顯的趨勢,但是也不想只做單邊、不想押注單邊。
  3. 看支撐壓力建倉:大盤移動過程中,用自己的方式判斷支撐、壓力點。接著,在壓力點往後N檔,可能100點、150點或200點,挑選一個合適的履約價賣出第1組價差單,接著再建立另外一邊價差單做對稱位置Iron Condor。

選擇權也有夜盤交易

  • 臺股期貨與選擇權一般交易時段:08:45~13:45
  • 臺股夜盤時段是:15:00~次日05:00

在夜盤時段都可交易選擇權:政府設立夜盤目的是讓投資人多一個避險管道。投資人能於夜盤進行交易。臺指期夜盤是跟著國際情勢波動,而夜盤主要交易量以專業投資人的自動交易程式為主。用手機APP可以看到夜盤期貨與選擇權報價,選『臺指近”全”』即可。細節可以參考 選擇權夜盤避險方式教學

選擇權建倉與平倉系統操作教學

選擇權建倉與平倉可以透過多次IOC功能,由電腦監測價差點數並自動連續嘗試建倉直到成交為止(就是洗單),多次IOC是選擇權交易人必用工具。

我們可以善用選擇權多次IOC下單功能來進行自動建倉與平倉,大幅降低盯盤時間。下方影片是多次IOC系統操作手把手教學。

認識Put Call Ratio

選擇權 Put Call Ratio(又寫做 P/C Ratio)是:把當日選擇權 Put未平倉量 除以 Call未平倉量。從選擇權賣方角度看,P/C Ratio數值越大通常看成偏多指標,因為Put未平倉量比Call未平倉量多,可以推斷賣Put多,更不容易跌破支撐;如果P/C Ratio越小則相反,代表賣CALL較多,看上漲遇壓不容易突破。賣出選擇權需壓保證金,賣方需要準備較多資金,所以Put Call Ratio是從賣方風險角度進行解讀。

選擇權教學結論

  1. 買選擇權要付出權利金,賣選擇權則收權利金。
  2. 買方要大漲、大跌才有機會以小博大,因為除了要看對方向,還要把付出的權利金賺回來。
  3. 買方付出權利金,就算看對方向也可能賠錢。
  4. 賣方當莊家,先收權利金,就算看錯方向也可能賺錢。
  5. 千萬不要裸賣選擇權。可透過同時持有買方賣方部位搭配使用,用交易策略提升勝率。

說人話的選擇權課程|一步步瞭解選擇權運作,打造長期穩定獲利

長期經營並製作中文市場最專業的選擇權教學網站,多年來我產出超過150個教學影片並回覆上千則留言,讓我完全清楚新手交易時遇到的問題與解決方法;我知道怎麼進行教學,最能幫助大家快速上手選擇權交易。今年是課程持續更新的第3年了,一次加入,永久更新

幾年前自己默默交易著選擇權,後來因為經營網站和YT頻道,開始認識一些業界前輩,有了更多交流。過程中學到很多選擇權策略,讓我能幫學員把這堂 說人話的選擇權課程 持續更新、優化。

最新動向!跟業界高手羊叔討論,我把『雙買+小臺』從原本的策略操作,延伸出更多配置來提高勝率,並免費更新給大家!

選擇權常見問題QA

為什麼要操作選擇權?

一是選擇權可以降低持倉成本,二是即使看錯,也不一定賠。假設大盤在17200,看臺積電上漲帶動大盤往上,可以賣出臺股選擇權 17000的賣權(賣PUT)同時買入臺積電。 萬一臺積電未如預期上漲,但臺股也沒下跌,結算時大盤點位在賣出的17000PUT之上,這口PUT還是獲利的:看錯也沒賠,還能降低臺積電持倉成本。

選擇權損平點怎麼看?

選擇權損平點計算方式:把履約價加上支出的權利金即是損平點。例如:買東西要花錢,當大盤在16200時,認為大盤會漲到16400,買進16400Call支出20點(臺幣$1000元),此時損平點是16420,最大虧損為20點,最大獲利為無限,但勝率低就是了。

裸賣是什麼?

「裸賣」顧名思義就是隻賣Call ,但沒做價差保護。交易人可以用『價差單』保護部位控制風險。

雙週選和月選的差別?

雙週選擇權每週三結算並有新的週合約,所以每個週三都同時有新、舊合約可交易;月選擇權是在每個月第3個星期三進行結算。市場上有時候會同時存在高達4份合約。以2022.11.23(三)為例,有:當天要結算的舊合約、當天新雙週選合約、將於下個週三結算的合約和12月的月選合約(如下圖)。img

怎麼建倉和平倉?可以掛單嗎?

選擇權建倉與平倉可以掛單,需透過多次IOC功能,由電腦監測價差點數並自動連續嘗試建倉直到成交為止(就是洗單),多次IOC是選擇權交易人必用工具。

from line_notify import LineNotify
from retry import retry

import datetime as dt
import sys
import platform
import signal
import datetime
import shioaji as sj
import os
import json
import pandas as pd
import requests

pd.options.display.float_format = lambda x: "%.2f" % x


class Watcher:
    def __init__(self):
        self.child = os.fork()
        if self.child == 0:
            return
        else:
            self.watch()

    def watch(self):
        try:
            os.wait()
        except KeyboardInterrupt:
            self.kill()
        sys.exit()

    def kill(self):
        try:
            print("kill")
            os.kill(self.child, signal.SIGKILL)
        except OSError:
            pass


@retry(exceptions=Exception, tries=3, delay=2, backoff=2)
def get_options_data(option_contract_period, queryStartDate=None, queryEndDate=None):
    if queryStartDate is None and queryEndDate is None:
        # now = datetime.datetime.now()
        now = datetime.datetime.now().time()  # 獲取當前時間的時間部分
        # now = datetime.datetime.now() - datetime.timedelta(days=1)
        # 如果超過15:00,日期加一天
        # if now.hour >= 15:
        #    now += datetime.timedelta(days=-1)
        queryStartDate = queryEndDate = now.strftime("%Y/%m/%d")
        if now < datetime.time(15, 0):  # 如果當前時間在 15:00 之前
            yesterday = datetime.date.today() - datetime.timedelta(days=1)  # 扣一天
            queryStartDate = queryEndDate = yesterday.strftime("%Y/%m/%d")
            print("目前時間在 15:00 之前,扣一天後為:", yesterday)

        # 轉換日期格式為年月日

    url = f"https://www.taifex.com.tw/cht/3/dlOptDataDown?down_type=1&commodity_id=txo&queryStartDate={queryStartDate}&queryEndDate={queryEndDate}"
    print(queryStartDate, queryEndDate, "\n", url)
    df = pd.read_csv(url, encoding="big5", index_col=False)
    # df = pd.read_csv("./夜盤.csv", encoding="big5", index_col=False)
    if not df.empty:
        df = df[
            ["交易日期", "契約", "買賣權", "到期月份(週別)", "履約價", "收盤價", "成交量", "交易時段", "未沖銷契約數"]
        ]
        df = df[df["收盤價"] != "-"]
        df["收盤價"] = df["收盤價"].astype(float)
        # df["收盤價"] = df["收盤價"].replace("-", "0").astype(float)
        df["成交量"] = df["成交量"].replace("-", "0").astype(float)
        df["未沖銷契約數"] = df["未沖銷契約數"].replace("-", "0").astype(float)
        df["履約價"] = df["履約價"].replace("-", "0").astype(float)
        df["到期月份(週別)"] = df["到期月份(週別)"].str.strip()
        df = df[
            (df["到期月份(週別)"] == option_contract_period)
            & (df["交易時段"] == "一般")
            & (df["收盤價"] > 0)
        ]
        # 新增未平倉金額欄位
        df["未平倉金額"] = df["收盤價"] * 50 * df["未沖銷契約數"]

        # 買權 DataFrame
        df_call = df[df["買賣權"] == "買權"]
        # 賣權 DataFrame
        df_put = df[df["買賣權"] == "賣權"]
        print(df.to_markdown(index=False, floatfmt=".2f"))
        df_call = df_call.sort_values("未平倉金額", ascending=False)
        print(df_call.to_markdown(index=False, floatfmt=".2f"))
        df_put = df_put.sort_values("未平倉金額", ascending=False)
        print(df_put.to_markdown(index=False, floatfmt=".2f"))
        return df
    else:
        return None


def get_TaiwanOptionDaily(symbol):
    date = datetime.datetime.now().time()  # 獲取當前時間的時間部分
    if date < datetime.time(15, 0):  # 如果當前時間在 15:00 之前
        date = (datetime.date.today() - datetime.timedelta(days=1)).strftime("%Y-%m-%d")
    else:
        date = date.strftime("%Y-%m-%d")
    url = "https://api.finmindtrade.com/api/v3/data"
    parameter = {"dataset": "TaiwanOptionDaily", "data_id": "TXO", "date": date}
    data = requests.get(url, params=parameter)
    data = data.json()
    df = pd.DataFrame(data["data"])
    df = df[
        # (df["contract_date"] == "202303W5")
        (df["contract_date"] == symbol)
        & (df["date"] == date)
        & (df["trading_session"] == "position")
        & (df["close"] != 0)
    ]
    df["未平倉資金"] = df["close"] * 50 * df["open_interest"]
    print(df.to_markdown(index=False, floatfmt=".2f"))
    # 依據 call_put 欄位分成兩個 DataFrame
    df_call = df[df["call_put"] == "call"]
    df_put = df[df["call_put"] == "put"]
    print(df_call)
    print(df_put)


def get_option_week(api):
    def get_previous_wednesday():
        today = datetime.datetime.today()
        wednesday = (
            today
            - datetime.timedelta(days=today.weekday())
            + datetime.timedelta(days=2)
        )
        previous_wednesday = wednesday - datetime.timedelta(days=7)
        return previous_wednesday.date()

    def get_this_week_wednesday():
        today = datetime.datetime.today()
        wednesday = (today + datetime.timedelta(days=(2 - today.weekday()))).date()
        return wednesday

    def get_next_week_wednesday():
        today = datetime.datetime.today()
        wednesday = (today + datetime.timedelta(days=(2 - today.weekday() + 7))).date()
        return wednesday

    for option in api.Contracts.Options:
        for contract in option:
            if "TX" in contract.category:
                now = datetime.datetime.now()
                wednesday_time = get_this_week_wednesday()
                wednesday_time = datetime.datetime.combine(
                    wednesday_time, datetime.datetime.min.time()
                ) + datetime.timedelta(
                    hours=14
                )  # 因為程式是14:50啟動計算所以改設定14:00
                # print(wednesday_time)

                # 根據當前時間判斷是否在星期三 15:00之前。如果在此時間之前,則列印上週三和本週三的日期;否則列印本週三和下週三的日期:
                if now < wednesday_time:
                    if datetime.datetime.strptime(
                        contract.update_date, "%Y/%m/%d"
                    ).date() >= get_previous_wednesday() and contract.delivery_date == get_this_week_wednesday().strftime(
                        "%Y/%m/%d"
                    ):
                        print(
                            contract.symbol,
                            contract.name,
                            contract.update_date,
                            contract.delivery_date,
                        )
                        return contract.symbol[:3]
                else:
                    if datetime.datetime.strptime(
                        contract.update_date, "%Y/%m/%d"
                    ).date() >= get_this_week_wednesday() and contract.delivery_date == get_next_week_wednesday().strftime(
                        "%Y/%m/%d"
                    ):
                        print(
                            contract.symbol,
                            contract.name,
                            contract.update_date,
                            contract.delivery_date,
                        )
                        return contract.symbol[:9]


if __name__ == "__main__":
    if platform.system().lower() == "linux":
        Watcher()

    with open(os.environ["HOME"] + "/.mybin/shioaji_token.txt", "r") as f:
        date = dt.datetime.now().strftime("%Y-%m-%d")
        api = sj.Shioaji()
        kw_login = json.loads(f.read())
        api.login(**kw_login, contracts_timeout=300000)
        symbol = get_option_week(api)
        if symbol[:3] == "TXO":
            option_contract_period = symbol[3:9]
            future_contract_code = "TXF"
        else:
            option_contract_period = symbol[3:9] + "W" + symbol[:3][-1]
            future_contract_code = "MX" + symbol[:3][-1]
            future_contract_period = future_contract_code + symbol[3:9]
            print(future_contract_code, future_contract_period)

            kbars = api.kbars(
                api.Contracts.Futures[future_contract_code][future_contract_period],
                date,
            )
            df = pd.DataFrame({**kbars})
            df.ts = pd.to_datetime(df.ts)
            df.set_index("ts", inplace=True)
            print(df)

        print(option_contract_period)
        get_TaiwanOptionDaily(option_contract_period)
        get_options_data(option_contract_period)

        TXF = (
            sorted([x for x in dir(api.Contracts.Futures.TXF) if x.startswith("TXF")])
        )[0]
        kbars = api.kbars(api.Contracts.Futures.TXF[TXF], date)
        df = pd.DataFrame({**kbars})
        df.ts = pd.to_datetime(df.ts)
        df.set_index("ts", inplace=True)
        print(df)
import requests
import pandas as pd

pd.options.display.float_format = lambda x: "%.2f" % x

url = "https://api.finmindtrade.com/api/v3/data"
parameter = {"dataset": "TaiwanOptionDaily", "data_id": "TXO", "date": "2023-03-20"}
data = requests.get(url, params=parameter)
data = data.json()
df = pd.DataFrame(data["data"])
df = df[
    (df["contract_date"] == "202303W5")
    & (df["date"] == "2023-03-22")
    & (df["trading_session"] == "position")
    & (df["close"] != 0)
]
df['未平倉資金'] = df['close'] * 50 * df['open_interest']
print(df.to_markdown(index=False, floatfmt=".2f"))

simple_from_signals

from binance.client import Client
from dateutil import relativedelta
import vectorbt as vbt
import datetime as dt
import pandas as pd
import os
import json
import warnings

warnings.simplefilter("ignore", UserWarning)


class binanceAPI:
    def __init__(self, configPath):
        with open(configPath, "r") as f:
            self.kw_login = json.loads(f.read())
        self.api = self.__login(self.kw_login["PUBLIC"], self.kw_login["SECRET"])
        self.__conncet_redis()

    def __conncet_redis(self):
        redis_info = {
            "host": "127.0.0.1",
            "port": 6379,
            "max_connections": 2,
            "db": 1,
        }
        pool = redis.ConnectionPool(**redis_info)
        try:
            self.rs = redis.Redis(connection_pool=pool)
        except Exception as err:
            logger.error(err)
            raise err

    def __login(self, PUBLIC, SECRET):
        return Client(api_key=PUBLIC, api_secret=SECRET)


def build_df(klines):
    cols = [
        "timestamp",
        "open",
        "high",
        "low",
        "close",
        "volume",
        "close_time",
        "quote_av",
        "trades",
        "tb_base_av",
        "tb_quote_av",
        "ignore",
    ]
    df = pd.DataFrame(klines, columns=cols)
    df["timestamp"] = [dt.datetime.fromtimestamp(x / 1000.0) for x in df["timestamp"]]
    df.set_index("timestamp", inplace=True)
    df = df[["open", "high", "low", "close", "volume"]]
    df[["open", "high", "low", "close", "volume"]] = df[
        ["open", "high", "low", "close", "volume"]
    ].astype(float)
    df["idx"] = range(0, len(df))
    return df


def backtesting(df):
    price = df["close"]
    fast_ma = vbt.MA.run(price, 5)
    slow_ma = vbt.MA.run(price, 50)
    entries = fast_ma.ma_crossed_above(slow_ma)
    exits = fast_ma.ma_crossed_below(slow_ma)
    show_report(df, entries, exits)


def show_report(df, long_entries, long_exits):
    pf = vbt.Portfolio.from_signals(
        df["close"],
        long_entries,
        long_exits,
        init_cash=100000,
        fees=0.000,
    )
    stats_info = pf.stats()
    start = stats_info["Start"]
    end = stats_info["End"]
    winrate = stats_info[r"Win Rate [%]"]
    total_return = stats_info[r"Total Return [%]"]
    total_profit = pf.total_profit()
    max_drawdown = pf.max_drawdown()
    total_trades = len(pf.trades)
    print(pf.stats())
    print(
        f"coin:{coin} {start} ~ {end}, winrate:{round(winrate, 2)}%, total_return:{round(total_return, 2)}, max_drawdown:{round(max_drawdown, 2)}, total_trades:{total_trades}"
    )


if __name__ == "__main__":
    coin = "BTCUSDT"
    configPath = os.environ["HOME"] + "/.mybin/jason/binance_login.txt"
    KLINE_INTERVAL = Client.KLINE_INTERVAL_1MINUTE
    with open(configPath, "r") as f:
        kw_login = json.loads(f.read())

    start_time = dt.datetime.now()
    client = Client(api_key=kw_login["PUBLIC"], api_secret=kw_login["SECRET"])
    klines = client.get_historical_klines(
        symbol=coin,
        interval=KLINE_INTERVAL,
        start_str=(start_time - relativedelta.relativedelta(months=1)).strftime(
            "%Y-%m-%d %H:%M:%S"
        ),
        end_str=start_time.strftime("%Y-%m-%d %H:%M:%S"),
    )
    df = build_df(klines)
    backtesting(df)

test_func_nb_order

from numba import njit
from binance.client import Client
from dateutil import relativedelta
from vectorbt.portfolio.enums import (
    Direction,
    NoOrder,
    OrderStatus,
    OrderSide,
)
from vectorbt.portfolio import nb
import datetime as dt
import json
import os
import numpy as np
import pandas as pd
import vectorbt as vbt
import warnings

warnings.filterwarnings("ignore")


@njit
def pre_sim_func_nb(c):
    # We need to define stop price per column once
    stop_price = np.full(c.target_shape[1], np.nan, dtype=np.float_)
    return (stop_price,)


@njit
def order_func_nb(c, stop_price, entries, exits, size):
    # Select info related to this order
    entry_now = nb.get_elem_nb(c, entries)
    exit_now = nb.get_elem_nb(c, exits)
    size_now = nb.get_elem_nb(c, size)
    price_now = nb.get_elem_nb(c, c.close)
    stop_price_now = stop_price[c.col]

    # Our logic
    if entry_now:
        if c.position_now == 0:
            return nb.order_nb(
                size=size_now, price=price_now, direction=Direction.LongOnly
            )
    elif exit_now or price_now >= stop_price_now:
        if c.position_now > 0:
            return nb.order_nb(
                size=-size_now, price=price_now, direction=Direction.LongOnly
            )
    return NoOrder


@njit
def post_order_func_nb(c, stop_price, stop):
    # Same broadcasting as for size
    stop_now = nb.get_elem_nb(c, stop)

    if c.order_result.status == OrderStatus.Filled:
        if c.order_result.side == OrderSide.Buy:
            # Position entered: Set stop condition
            stop_price[c.col] = (1 + stop_now) * c.order_result.price
        else:
            # Position exited: Remove stop condition
            stop_price[c.col] = np.nan


def simulate(close, entries, exits, size, threshold):
    return vbt.Portfolio.from_order_func(
        close,
        order_func_nb,
        vbt.Rep("entries"),
        vbt.Rep("exits"),
        vbt.Rep("size"),  # order_args
        pre_sim_func_nb=pre_sim_func_nb,
        post_order_func_nb=post_order_func_nb,
        post_order_args=(vbt.Rep("threshold"),),
        broadcast_named_args=dict(  # broadcast against each other
            entries=entries, exits=exits, size=size, threshold=threshold
        ),
    )


def build_df(klines):
    cols = [
        "timestamp",
        "open",
        "high",
        "low",
        "close",
        "volume",
        "close_time",
        "quote_av",
        "trades",
        "tb_base_av",
        "tb_quote_av",
        "ignore",
    ]
    df = pd.DataFrame(klines, columns=cols)
    df["timestamp"] = [dt.datetime.fromtimestamp(x / 1000.0) for x in df["timestamp"]]
    df.set_index("timestamp", inplace=True)
    df = df[["open", "high", "low", "close", "volume"]]
    df[["open", "high", "low", "close", "volume"]] = df[
        ["open", "high", "low", "close", "volume"]
    ].astype(float)
    return df


def test():
    close = pd.Series([10, 11, 12, 13, 14])
    entries = pd.Series([True, True, False, False, False])
    exits = pd.Series([False, False, False, True, True])
    pf = simulate(close, entries, exits, np.inf, 0.1)  # .asset_flow()
    print(pf.asset_flow())
    print(pf.stats())


if __name__ == "__main__":
    # test()

    coin = "BTCUSDT"
    configPath = os.environ["HOME"] + "/.mybin/jason/binance_login.txt"
    KLINE_INTERVAL = Client.KLINE_INTERVAL_1MINUTE
    with open(configPath, "r") as f:
        kw_login = json.loads(f.read())

    start_time = dt.datetime.now()
    client = Client(api_key=kw_login["PUBLIC"], api_secret=kw_login["SECRET"])
    klines = client.get_historical_klines(
        symbol=coin,
        interval=KLINE_INTERVAL,
        start_str=(start_time - relativedelta.relativedelta(months=1)).strftime(
            "%Y-%m-%d %H:%M:%S"
        ),
        end_str=start_time.strftime("%Y-%m-%d %H:%M:%S"),
    )
    df = build_df(klines)
    price = df["close"]
    fast_ma = vbt.MA.run(price, 5)
    slow_ma = vbt.MA.run(price, 50)
    entries = fast_ma.ma_crossed_above(slow_ma)
    exits = fast_ma.ma_crossed_below(slow_ma)
    pf = simulate(price, entries, exits, np.inf, 0.1)  # .asset_flow()
    print(pf.stats())

from_orders

import pandas as pd
import vectorbt as vbt
import numpy as np
from datetime import datetime, timedelta


# Entry trades
pf_kwargs = dict(
    close=pd.Series([1., 2., 3., 4., 5.]),
    size=pd.Series([1., -2., 2., -2., 1.]),
    fixed_fees=1.
)

entry_trades = vbt.Portfolio.from_orders(**pf_kwargs).entry_trades
print(entry_trades.records_readable)
exit_trades = vbt.Portfolio.from_orders(**pf_kwargs).exit_trades
print(exit_trades.records_readable)

# Entry positions
positions = vbt.Portfolio.from_orders(**pf_kwargs).positions
print(positions.records_readable)

print(entry_trades.pnl.sum() == exit_trades.pnl.sum() == positions.pnl.sum())

price = pd.Series([1., 2., 3., 4., 3., 2., 1.])
size = pd.Series([1., -0.5, -0.5, 2., -0.5, -0.5, -0.5])
trades = vbt.Portfolio.from_orders(price, size).trades

print(trades.count())
print(trades.pnl.sum())
print(trades.winning.count())
print(trades.winning.pnl.sum())
print(trades.stats())

np.random.seed(42)
price = pd.DataFrame({
    'a': np.random.uniform(1, 2, size=100),
    'b': np.random.uniform(1, 2, size=100)
}, index=[datetime(2020, 1, 1) + timedelta(days=i) for i in range(100)])
size = pd.DataFrame({
    'a': np.random.uniform(-1, 1, size=100),
    'b': np.random.uniform(-1, 1, size=100),
}, index=[datetime(2020, 1, 1) + timedelta(days=i) for i in range(100)])
pf = vbt.Portfolio.from_orders(price, size, fees=0.01, freq='d')
print(pf.trades['a'].stats(settings=dict(incl_open=True)))

多幣種回測

import numpy as np
import pandas as pd
import vectorbt as vbt
import warnings
from datetime import datetime


# Prepare data
start = "2019-01-01 UTC"  # crypto is in UTC
end = "2020-01-01 UTC"
btc_price = vbt.YFData.download("BTC-USD", start=start, end=end).get("Close")
eth_price = vbt.YFData.download("ETH-USD", start=start, end=end).get("Close")
comb_price = btc_price.vbt.concat(
    eth_price, keys=pd.Index(["BTC", "ETH"], name="symbol")
)
comb_price.vbt.drop_levels(-1, inplace=True)

fast_ma = vbt.MA.run(comb_price, [10, 20], short_name="fast")
slow_ma = vbt.MA.run(comb_price, [30, 30], short_name="slow")

entries = fast_ma.ma_crossed_above(slow_ma)
exits = fast_ma.ma_crossed_below(slow_ma)

pf = vbt.Portfolio.from_signals(comb_price, entries, exits)
print(pf.total_return())
print(pf.stats())

Multiple assets, multiple trade signals per asset

import pandas as pd
import vectorbt as vbt

price = pd.DataFrame({"p1": [1, 2, 3, 4], "p2": [5, 6, 7, 8]})
price.columns.name = "asset"
entries = pd.DataFrame(
    {
        "en1": [True, False, False, False],
        "en2": [False, True, False, False],
        "en3": [False, False, True, False],
        "en4": [False, False, False, True],
    }
)
entries.columns.name = "entries"
exits = pd.DataFrame(
    {
        "ex1": [False, False, False, True],
        "ex2": [False, False, False, True],
        "ex3": [False, False, False, True],
        "ex4": [False, False, False, True],
    }
)
exits.columns.name = "exits"
entries = entries.vbt.stack_index(pd.Index(["p1", "p1", "p2", "p2"], name="asset"))
exits = exits.vbt.stack_index(pd.Index(["p1", "p1", "p2", "p2"], name="asset"))
portfolio = vbt.Portfolio.from_signals(price, entries, exits)  # not grouped portfolio
print(portfolio.total_return())
print(portfolio.total_return(group_by='asset'))  # group not grouped portfolio

portfolio = vbt.Portfolio.from_signals(price, entries, exits, group_by='asset')  # grouped portfolio
print(portfolio.total_return())

print(portfolio.total_return(group_by=False))  # ungroup grouped portfolio

from_order_func 做資金加減碼

from numba import njit
from vectorbt.portfolio import nb
from vectorbt.portfolio.enums import Direction

import numpy as np
import vectorbt as vbt
import pandas as pd
import warnings

pd.options.display.float_format = lambda x: "%.2f" % x

warnings.simplefilter("ignore", UserWarning)


def simulate():
    @njit
    def order_func_nb(c, action, direction, fees):
        # _size = 1000 / float(c.close[c.i, c.col])
        # print(
        #    "Close:",
        #    c.close[c.i, c.col],
        #    "Direction:",
        #    direction,
        #    "c.i:",
        #    c.i,
        #    "c.col:",
        #    c.col,
        #    "fees:",
        #    fees,
        #    "_size:",
        #    round(_size, 2),
        #    "position_now:",
        #    c.position_now,
        #    "action:",
        #    action[c.i],
        # )

        size = 0
        if action[c.i] == 1:
            # 1000 / float(c.close[c.i, c.col]) 買入 1000 元的股票
            size = 1000 / float(c.close[c.i, c.col])
        elif action[c.i] == -1:
            # -c.position_now 持有全部的股票賣出
            size = -c.position_now

        return nb.order_nb(
            price=c.close[c.i, c.col], size=size, direction=direction, fees=fees,
        )

    # 加碼策略
    action = pd.Series([1, 1, -1, 1, -1, 0, 0, 1, -1])
    dates = pd.date_range("20220301", periods=len(action))
    price = pd.DataFrame(
        {"Price": [100, 200, 300, 400, 500, 600, 700, 800, 900]}, index=dates
    )
    fees = 0.002  # per frame
    pf = vbt.Portfolio.from_order_func(
        price,
        order_func_nb,
        np.asarray(action),
        Direction.LongOnly,
        fees,
        init_cash=1000000,
    )
    orders_records_readable = pf.orders.records_readable.drop("Column", axis=1)
    print(orders_records_readable.to_markdown(index=False, floatfmt=".2f"))
    print(pf.assets().rename(columns={"Price": "assets"}))
    print(pf.cash().rename(columns={"Price": "cash"}))
    print(pf.stats())


if __name__ == "__main__":
    simulate()

OrderContext

class OrderContext(tp.NamedTuple):
    target_shape: tp.Shape  # 目標形狀
    group_lens: tp.Array1d  # 分組長度
    init_cash: tp.Array1d  # 初始現金
    cash_sharing: bool  # 是否共享現金
    call_seq: tp.Optional[tp.Array2d]  # 呼叫順序
    segment_mask: tp.ArrayLike  # 分段遮罩
    call_pre_segment: bool  # 是否在分段之前呼叫
    call_post_segment: bool  # 是否在分段之後呼叫
    close: tp.ArrayLike  # 收盤價
    ffill_val_price: bool  # 是否向前填充估值價格
    update_value: bool  # 是否更新持倉估值
    fill_pos_record: bool  # 是否填充持倉紀錄
    flex_2d: bool  # 是否彈性處理2D數據
    order_records: tp.RecordArray  # 訂單紀錄
    log_records: tp.RecordArray  # 日誌紀錄
    last_cash: tp.Array1d  # 上次現金
    last_position: tp.Array1d  # 上次持倉
    last_debt: tp.Array1d  # 上次負債
    last_free_cash: tp.Array1d  # 上次自由現金
    last_val_price: tp.Array1d  # 上次估值價格
    last_value: tp.Array1d  # 上次持倉估值
    second_last_value: tp.Array1d  # 倒數第二次持倉估值
    last_return: tp.Array1d  # 上次收益率
    last_oidx: tp.Array1d  # 上次訂單索引
    last_lidx: tp.Array1d  # 上次日誌索引
    last_pos_record: tp.RecordArray  # 上次持倉紀錄
    group: int  # 分組
    group_len: int  # 分組長度
    from_col: int  # 起始欄位
    to_col: int  # 終止欄位
    i: int  # 迭代器
    call_seq_now: tp.Optional[tp.Array1d]  # 當前呼叫順序
    col: int  # 當前欄位
    call_idx: int  # 當前呼叫索引
    cash_now: float  # 現金餘額
    position_now: float  # 持倉量
    debt_now: float  # 負債金額
    free_cash_now: float  # 自由現金餘額
    val_price_now: float  # 估值價格
    value_now: float  # 持倉估值
    return_now: float  # 當前收益率
報告列出了許多評估指標:

Start:回測開始日期。
End:回測結束日期。
Period:回測時間段。
Start Value:回測開始時的資產價值。
End Value:回測結束時的資產價值。
Total Return [%]:回測期間的總回報率。
Benchmark Return [%]:基準指數的回報率。
Max Gross Exposure [%]:最大總槓桿率。
Total Fees Paid:交易費用總額。
Max Drawdown [%]:最大回撤率。
Max Drawdown Duration:最大回撤期間。
Total Trades:總交易次數。
Total Closed Trades:總平倉交易次數。
Total Open Trades:總持倉交易次數。
Open Trade PnL:未平倉交易的盈虧。
Win Rate [%]:勝率。
Best Trade [%]:最佳交易回報率。
Worst Trade [%]:最差交易回報率。
Avg Winning Trade [%]:平均勝利交易回報率。
Avg Losing Trade [%]:平均虧損交易回報率。
Avg Winning Trade Duration:平均勝利交易持續時間。
Avg Losing Trade Duration:平均虧損交易持續時間。
Profit Factor:盈虧比。
Expectancy:預期值。
Sharpe Ratio:夏普比率。
Calmar Ratio:卡爾馬比率。
Omega Ratio:歐米茄比率。
Sortino Ratio:索提諾比率。

其中一些指標的定義可能需要參考具體的金融概念,例如回報率、總槓桿率、回撤率、夏普比率等等。這些指標可以幫助用戶評估交易策略的表現,以便做出相應的調整和優化。

asset_flow

import numpy as np
from vectorbt.records.nb import col_map_nb
from vectorbt.portfolio.nb import simulate_from_orders_nb, asset_flow_nb
from vectorbt.portfolio.enums import Direction

close = np.array([1, 2, 3, 4, 5])[:, None]
order_records, _ = simulate_from_orders_nb(
    target_shape=close.shape,
    close=close,
    group_lens=np.array([1]),
    init_cash=np.array([100]),
    call_seq=np.full(close.shape, 0)
)

print(order_records)

col_map = col_map_nb(order_records['col'], close.shape[1])
asset_flow = asset_flow_nb(close.shape, order_records, col_map, Direction.Both)
print(asset_flow)

資金1000 加碼 size 統計

import pandas as pd

# create a date range
dates = pd.date_range("20220301", periods=7)

# create a price dataframe
price = pd.DataFrame({"price": [50, 100, 200, 250, 300, 400, 500]}, index=dates)

# create entry and exit dataframes
entries = pd.DataFrame(
    {"entry": [False, True, True, False, False, True, False]}, index=dates
)
exits = pd.DataFrame(
    {"exit": [False, False, False, False, True, False, True]}, index=dates
)

# concatenate the dataframes horizontally
df = pd.concat([price, entries, exits], axis=1)

# calculate the size column based on entries and exits
size = []
current_size = 0
for i in range(len(df)):
    if df["entry"][i]:
        current_size = 1000 / df["price"][i]
    elif df["exit"][i]:
        current_size = -sum(size)
    elif not df["entry"][i] and not df["exit"][i]:
        current_size = 0
    size.append(current_size)

# add the size column to the dataframe
df["size"] = size

# print the resulting dataframe
print(df)
from numba import njit
from vectorbt.portfolio import nb
from vectorbt.portfolio.enums import (
    SizeType,
    Direction,
    NoOrder,
    OrderStatus,
    OrderSide,
)


import numpy as np
import vectorbt as vbt
import pandas as pd
import warnings

warnings.simplefilter("ignore", UserWarning)


@njit
def pre_sim_func_nb(c):
    # We need to define stop price per column once
    stop_price = np.full(c.target_shape[1], np.nan, dtype=np.float_)
    return (stop_price,)


@njit
def order_func_nb(c, stop_price, entries, exits, size):
    # Select info related to this order
    entry_now = nb.get_elem_nb(c, entries)
    exit_now = nb.get_elem_nb(c, exits)
    size_now = nb.get_elem_nb(c, size)
    price_now = nb.get_elem_nb(c, c.close)
    stop_price_now = stop_price[c.col]

    # Our logic
    if entry_now:
        if c.position_now == 0:
            return nb.order_nb(
                size=size_now, price=price_now, direction=Direction.LongOnly
            )
    elif exit_now or price_now >= stop_price_now:
        if c.position_now > 0:
            return nb.order_nb(
                size=-size_now, price=price_now, direction=Direction.LongOnly
            )
    return NoOrder


@njit
def post_order_func_nb(c, stop_price, stop):
    # Same broadcasting as for size
    stop_now = nb.get_elem_nb(c, stop)

    if c.order_result.status == OrderStatus.Filled:
        if c.order_result.side == OrderSide.Buy:
            # Position entered: Set stop condition
            stop_price[c.col] = (1 + stop_now) * c.order_result.price
        else:
            # Position exited: Remove stop condition
            stop_price[c.col] = np.nan


def simulate(close, entries, exits, size, threshold):
    return vbt.Portfolio.from_order_func(
        close,
        order_func_nb,
        vbt.Rep("entries"),
        vbt.Rep("exits"),
        vbt.Rep("size"),  # order_args
        pre_sim_func_nb=pre_sim_func_nb,
        post_order_func_nb=post_order_func_nb,
        post_order_args=(vbt.Rep("threshold"),),
        broadcast_named_args=dict(  # broadcast against each other
            entries=entries, exits=exits, size=size, threshold=threshold
        ),
    )


if __name__ == "__main__":
    close = pd.Series([10, 11, 12, 13, 14])
    entries = pd.Series([True, True, False, False, False])
    exits = pd.Series([False, False, False, True, True])
    pf = simulate(close, entries, exits, np.inf, 0.1)
    print(pf.orders.records_readable)
    print(pf.assets())
    print(pf.cash())
    print(pf.stats())

使用 from_order_func 動態加碼

from numba import njit
from vectorbt.utils.enum_ import map_enum_fields
from vectorbt.portfolio import nb
from vectorbt.portfolio.enums import (
    SizeType,
    Direction,
    NoOrder,
    OrderStatus,
    OrderSide,
)


import yfinance as yf
import numpy as np
import vectorbt as vbt
import pandas as pd
import warnings

warnings.simplefilter("ignore", UserWarning)

# 下載股票價格數據
# symbols = ["GOOG"]
# raw_data = yf.download(symbols, start="2010-01-01", end="2023-03-09")
# close = raw_data.loc[:, "Close"]

# Buy 10 units each tick using closing price:
def test():
    @njit
    def order_func_nb(c, size):
        return nb.order_nb(size=size)

    close = pd.Series([1, 2, 3, 4, 5])
    pf = vbt.Portfolio.from_order_func(close, order_func_nb, 10, init_cash=100000,)
    print(pf.assets())
    print(pf.cash())
    print(pf.stats())


def from_order_function_test():
    @njit
    def order_func_nb(c, size, direction, fees):
        print(
            "Close:",
            c.close[c.i, c.col],
            "Size:",
            size[c.i],
            "Direction:",
            direction[c.col],
            "c.i:",
            c.i,
            "c.col:",
            c.col,
            "fees:",
            fees,
        )
        return nb.order_nb(
            price=c.close[c.i, c.col],
            size=size[c.i],
            direction=direction[c.col],
            fees=fees,
        )

    if True:
        # 加碼策略
        size = pd.Series([1, 1, -2, 1, -1])  # per row
        dates = pd.date_range("20220301", periods=5)
        price = pd.DataFrame(
            {"a": [100, 200, 300, 400, 500], "b": [500, 400, 300, 200, 100]},
            index=dates,
        )  # per element
    else:
        size = pd.Series([1, -1, 1, -1])  # per row
        dates = pd.date_range("20220301", periods=4)
        price = pd.DataFrame(
            {"a": [100, 200, 300, 400], "b": [400, 300, 200, 100]}, index=dates,
        )  # per element

    direction = ["longonly", "shortonly"]  # per column
    fees = 0.01  # per frame
    direction_num = map_enum_fields(direction, Direction)
    pf = vbt.Portfolio.from_order_func(
        price,
        order_func_nb,
        np.asarray(size),
        np.asarray(direction_num),
        fees,
        init_cash=10000,
    )
    print(pf.orders.records_readable)
    print(pf.assets())
    print(pf.cash())
    print(pf.stats())


if __name__ == "__main__":
    # Disable scientific notation
    pd.options.display.float_format = lambda x: "%.2f" % x
    test()
    print("\n=================================================\n")
    from_order_function_test()

VectotBT example

from datetime import datetime
import vectorbt as vbt

interval = '4h'
cols = ['Open', 'High', 'Low', 'Close', 'Volume']
start_str = '360 days ago UTC'
end_str = f'{datetime.now()}'
#symbols = ["BTCUSDT", "ETHUSDT", "LTCUSDT", "BNBUSDT", "XRPUSDT"]
symbols = ["BTCUSDT"]
df = vbt.BinanceData.download(symbols,start=start_str,interval=interval).get(cols)
ma99 = vbt.MA.run(df["Close"], 99, short_name="ma99")

entries = ma99.close_crossed_above(ma99.ma) 
exits = ma99.close_crossed_below(ma99.ma) 
pf = vbt.Portfolio.from_signals(df["Close"], entries, exits)
print(pf.stats())

Python筆記 : 股票策略回測 by Vectorbt

大致介紹

vectorbt是一套拿來進行量化分析的套件,特別的點在於他有numpy的速度,以及pandas的方便性。 因此比起其他的回測套件,他擁有極佳的速度,可以在短時間之內分析大量的策略。其中,套到這個套件裡面的所有參數都可以進行向量化,允許我們同時對所有元素執行相同的操作。另外,也使用 Numba 解決了與向量化相關的路徑依賴問題。

這邊大部分使用的套件說明都可以在這裡看到詳細說明。

模組

import vectorbt as vbt 
# import plotly 可不用,因vectorbt有自帶視覺化套件
import datetime
import pandas as pd
import numpy as np

時間設定以及視覺化分析資料的位置

filename='某個位置路徑'
end_date=datetime.datetime.now()
start_date=end_date - datetime.timedelta(days=3) #限制在3天之內

這邊設3天是因為等等要抓取的時間單位為一分鐘,故三天已經夠長了。

Data Import

btc_price = vbt.YFData.download('BTC-USD',  
                                #['BTC-USD','ETH-USD'], 可以用列表同時import多重的data
                                interval='1m',  #改變時間的單位
                                start = start_date,
                                end=end_date,
                                missing_idnex='drop').get('Close') 

vectorbt會自動抓取yfinace的api,只要yfinance有的,都可以抓的到。 標的可以分為:

臺股美股加密貨幣
Ex : 2330.TWEx : TSLAEx : BTC-USD

交易策略

這裡使用的交易策略相當簡單,因為主要目的是為了熟悉vectorbt的使用。

情況操作
rsi>設定的高標賣出
rsi<設定的低標 且 最近的收盤價低於ma買入

當rsi大於我們設定的高標,代表目前股市可能進入過熱的壯臺,則我們可以選擇在這時候進行賣出的操作。當相反情況出現,rsi低於我們設定的低標的話,而且這時候的收盤價低於ma線,表示這時候或許是一個很好的入場點,則我們可以在這時候進行買入的操作。(這邊都是極度簡單的操作,要是這樣就可以穩穩贏過大盤就太感謝了…)

客製化訊號以及策略

這邊主要可以分成三個區塊,分別為:

  1. Define一個函數,裡面主要用來計算出是否可以進入市場的訊號 -> 1及-1。
  2. 算是一個用來做出策略的食譜,裡面放著等等產出的資料變數名稱以及一些默認變數。
  3. 實際執行策略,裡麵包含一些變數的範圍。

1. Define a function

def custom_indicator(close,               #每當在這邊增加一個參數,就要在第二步驟的ind裡面的param_names裡面增加
                     rsi_window = 14,
                     ma_window = 50,
                     entry=30,
                     exits=70
                     ):    
    close_5m = close.resample('5T').last() #將資料型態從1min變成5min
    rsi = vbt.RSI.run(close_5m, window = rsi_window).rsi  #rsi中的rsi值(因為在這裡的前面rsi出來的不會只有單純的rsi)
    rsi, _ =rsi.align(close,    #將5min的資料重新展開成1min
                     broadcast_axis=0,
                     method='ffill',  #並將空值以第5分鐘的copy塞進去
                     join='right'  #rsi是right table,close是left table
                     ) #by doing so, rsi跟close有same shape,只是rsi是5min的資料,close是1分鐘的資料
    close = close.to_numpy()
    rsi = rsi.to_numpy()
    ma = vbt.MA.run(close, window = ma_window).ma.to_numpy()
    trend = np.where(rsi > exits, -1, 0) #要是rsi>70,則賣出(-1),否則甚麼都不做(0)
    trend = np.where((rsi < entry)&(close < ma),  1, trend)  #要是rsi<30而且收盤價<ma,則買入(1),如果沒有的話就按照原本的trend
    return trend

2. 策略的食譜

ind = vbt.IndicatorFactory(   
    class_name = 'Combination',
    short_name = 'comb', 
    input_names=['close'], #輸入的parameter的名字
    #param_names=['window'], #hyper parameter的名字,也可用列表呈現
    param_names=['rsi_window','ma_window','entry','exits'],
    output_names=['value']  #output出來的名字
    ).from_apply_func(   #提供一些默認值給recipe,就是上面define的function裡面的parameter
        custom_indicator,
        rsi_window=14,
        ma_window=50,
        entry=70,
        exits=30,
        keep_pd=True #保持資料型態為pandas,避免變成numpy arrays
        )

3. 實際執行策略

res = ind.run(  #run 一個策略
    btc_price,
    #rsi_window = [14,35,21], #給定特定值
    rsi_window = np.arange(10,40,step=3,dtype=int), #給定一個範圍
    #ma_window = [21,50,100],
    ma_window = np.arange(20,200,step=15,dtype=int),
    #entry=[30,40],
    entry = np.arange(10,40,step=4,dtype=int),
    #exits=[60,70],
    exits = np.arange(60,85,step=4,dtype=int),
    param_product=True) #如果沒有這個的話,只會按照list的順序,並不會兩兩配對

這邊有兩種險方式來定義變數,一個是給定特定值的list,Ex:[14,35,21],表示將rsi_window分別設定為14、35、21,代表只會嘗試這三個數值。另一種方式則是使用np.arange,將在這個range的數值全部帶進去進行計算,進而求出每一種狀況下的獲利。

執行結果

print(res.value)

output:

comb_rsi_window                10                  ...      37                
comb_ma_window                20                   ...     185                
comb_entry                     10                  ...      38                
comb_exits                     60              64  ...      80      84        
symbol                    BTC-USD ETH-USD BTC-USD  ... ETH-USD BTC-USD ETH-USD
Datetime                                           ...                        
2022-08-11 03:07:00+00:00       0       0       0  ...       0       0       0
2022-08-11 03:09:00+00:00       0       0       0  ...       0       0       0
2022-08-11 03:11:00+00:00       0       0       0  ...       0       0       0
2022-08-11 03:13:00+00:00       0       0       0  ...       0       0       0
2022-08-11 03:15:00+00:00       0       0       0  ...       0       0       0
                          ...     ...     ...  ...     ...     ...     ...
2022-08-14 03:00:00+00:00       0       0       0  ...       0       0       0
2022-08-14 03:01:00+00:00       0       0       0  ...       0       0       0
2022-08-14 03:02:00+00:00       0       0       0  ...       0       0       0
2022-08-14 03:03:00+00:00       0       0       0  ...       0       0       0
2022-08-14 03:04:00+00:00       0       0       0  ...       0       0       0

[3690 rows x 13440 columns]

如果將結果攤開來看,會呈現

img

以及

img

代表著我們進出場的訊號。

將訊號帶入策略執行

這時我們將剛剛得到的res進行處裡,其中1設定為entries,-1設定為exits。

entries = res.value == 1
exits = res.value == -1

實際執行

pf = vbt.Portfolio.from_signals(btc_price, entries, exits)

試著看看執行結果

print(pf.stats().to_string()) #to_string()可以將全部結果攤開

會得到:

Output from spyder call 'get_namespace_view':
Start                         2022-08-11 03:07:00+00:00
End                           2022-08-14 03:04:00+00:00
Period                                             3690
Start Value                                       100.0
End Value                                    100.179789
Total Return [%]                               0.179789
Benchmark Return [%]                           3.590516
Max Gross Exposure [%]                        83.020833
Total Fees Paid                                     0.0
Max Drawdown [%]                               2.078304
Max Drawdown Duration                       2193.187668
Total Trades                                    4.26994
Total Closed Trades                            3.966369
Total Open Trades                              0.303571
Open Trade PnL                                 0.076599
Win Rate [%]                                  63.829362
Best Trade [%]                                 0.680632
Worst Trade [%]                                -0.39365
Avg Winning Trade [%]                          0.607418
Avg Losing Trade [%]                            -0.6351
Avg Winning Trade Duration                   260.345941
Avg Losing Trade Duration                    445.916595
Profit Factor                                       inf
Expectancy                                     0.202725

執行此段程式碼則可以獲得所有變數組合的報酬

print(pf.total_return().to_string())

只擷取其中一小段

                 170             10          60          BTC-USD    0.000000
                                                         ETH-USD    0.000000
                                             64          BTC-USD    0.000000
                                                         ETH-USD    0.000000
                                             68          BTC-USD    0.000000
                                                         ETH-USD    0.000000
                                             72          BTC-USD    0.000000
                                                         ETH-USD    0.000000
                                             76          BTC-USD    0.000000
                                                         ETH-USD    0.000000
                                             80          BTC-USD    0.000000
                                                         ETH-USD    0.000000
                                             84          BTC-USD    0.000000
                                                         ETH-USD    0.000000
                                 14          60          BTC-USD    0.000000
                                                         ETH-USD    0.000000
                                             64          BTC-USD    0.000000
                                                         ETH-USD    0.000000
                                             68          BTC-USD    0.000000
                                                         ETH-USD    0.000000
                                             72          BTC-USD    0.000000
                                                         ETH-USD    0.000000
                                             76          BTC-USD    0.000000
                                                         ETH-USD    0.000000
                                             80          BTC-USD    0.000000
                                                         ETH-USD    0.000000
                                             84          BTC-USD    0.000000
                                                         ETH-USD    0.000000

只抽取此策略的報酬率

returns = pf.total_return()
print(returns.max()) #最大的報酬率
print(returns.idxmax()) #最大的組合
print(returns.to_string()) #所有組合

可以得到

0.06338762101981515 
(37, 20, 30, 84, 'ETH-USD') #分別代表rsi_window, ma_window, entry, exits 

資料視覺化

vectorbt套件中有附兩種資料視覺化的模式,分別為:

  1. Heatmap
  2. Volume

Heatmap

fig = returns.vbt.heatmap(
    x_level = 'comb_rsi_window',
    #y_level = 'comb_ma_window',
    y_level = 'comb_entry',
    slider_level = 'symbol' #如果同時分析不同標的,則可以透過slider切換
    )
fig.write_html(filename,auto_open=True)#圖片儲存並自動展開

img

Volume

將資料以3d的樣式呈現,可同時比較多個變數。

fig = returns.vbt.volume(
    x_level = 'comb_exits',
    y_level = 'comb_ma_window',
    z_level = 'comb_entry',
    slider_level = 'symbol'
    )
fig.write_html(filename,auto_open=True)#圖片儲存並自動展開

img

vectorbt 多空範例

from numba import njit
from vectorbt.utils.enum_ import map_enum_fields
from vectorbt.portfolio import nb
from vectorbt.portfolio.enums import Direction, NoOrder

import numpy as np
import vectorbt as vbt
import pandas as pd
import warnings

warnings.simplefilter("ignore", UserWarning)


def from_order_function_test():
    @njit
    def order_func_nb(c, size, direction, fees):
        print(
            "Close:",
            c.close[c.i, c.col],
            "Size:",
            size[c.i],
            "Direction:",
            direction[c.col],
            "c.i:",
            c.i,
            "c.col:",
            c.col,
            "fees:",
            fees,
            "position_now:",
            c.position_now,
        )
        return nb.order_nb(
            price=c.close[c.i, c.col],
            size=size[c.i],
            direction=direction[c.col],
            fees=fees,
        )

    if True:
        # 加碼策略
        size = pd.Series([1, 1, -2, 1, -1])  # per row
        dates = pd.date_range("20220301", periods=5)
        price = pd.DataFrame(
            {"a": [100, 200, 300, 400, 500], "b": [500, 400, 300, 200, 100]},
            index=dates,
        )  # per element
    else:
        size = pd.Series([1, -1, 1, -1])  # per row
        dates = pd.date_range("20220301", periods=4)
        price = pd.DataFrame(
            {"a": [100, 200, 300, 400], "b": [400, 300, 200, 100]},
            index=dates,
        )  # per element

    direction = ["longonly", "shortonly"]  # per column
    fees = 0.01  # per frame
    direction_num = map_enum_fields(direction, Direction)
    # size, direction, fees
    pf = vbt.Portfolio.from_order_func(
        price,
        order_func_nb,
        np.asarray(size),
        np.asarray(direction_num),
        fees,
        init_cash=10000,
    )
    print(pf.orders.records_readable.to_markdown(tablefmt="heavy_grid"))
    print(pf.assets().to_markdown(tablefmt="heavy_grid"))
    print(pf.cash().to_markdown(tablefmt="heavy_grid"))
    print(pf.stats().to_markdown(tablefmt="heavy_grid"))


if __name__ == "__main__":
    pd.options.display.float_format = lambda x: "%.2f" % x
    from_order_function_test()

量化交易學習-訂單簿建模

出處:https://zhuanlan.zhihu.com/p/499342831

相關資料蒐集:

Quant最愛:【HFT系列】基於機器學習的動態高頻限價訂單簿框架(Tick資料)

Optimal high-frequency market making strategy research based on limit order book

https://github.com/timothyyu/gdax-orderbook-ml

R語言對高頻交易訂單流進行建模分析 3

R語言對高頻交易訂單流進行建模分析 4

Quant最愛:重構訂單簿!基於深度學習的A股Tick級價格變動預測

數量技術宅:股指期貨高頻資料機器學習預測

張楚珩:【強化學習 187】Order Book Trading + RL

陳穎:基於高頻limit order book資料的短程價格方向預測——via multi-class SVM

文兄:【量化策略】基於Level 2高頻資料的機器學習預測研究

Haitian Wei:orderbook模型論文筆記

基於深度學習的股票限價訂單簿中間價格預測研究

基於Order Book的簡單特徵:以Optiver競賽為例

基於Order Book的深度學習模型:預測多時間段收益序列

https://towardsdatascience.com/price-impact-of-order-book-imbalance-in-cryptocurrency-markets-bf39695246f6

相關的論文可是老多了,挑幾個:

Investigating Limit Order Book Characteristics for Short Term Price Prediction: a Machine Learning Approach

https://arxiv.org/abs/2007.07319

DeepLOB: Deep Convolutional Neural Networks for Limit Order Books

一張圖解釋訂單簿:

img

可能能用的特徵:

  1. 基本:K線、交易量、大單交易量、趨勢指標等。
  2. 訂單簿快照資料,價格、數量、訂單數量、訂單持續時間
  3. 買單賣單跨度,買單賣單均值
  4. 加權平均價格
  5. 訂單價格差,累計加權價格差
  6. 買賣訂單密度,分佈刻畫。泊松分佈建模
  7. 訂單分佈的變化,變化速率
  8. 時間相關:每檔變化量,變化率,買賣檔位的變化差異。一段時間的新增限價單、市價單、取消單的總量,當前時段相對歷史的比例,總量變化率。對數收益率

模型目標:

  1. 趨勢分類:一段時間後的價格變化,分類模型
  2. 做市商演算法:AS模型的保留價格與最優差價

問題:

  1. 模型低訊號雜訊比,如何進行設計
  2. 間隔時間的參數調優,論文說2horizon最好。標的物的流動性建模。

5 個步驟設定選股條件,股票爆發力更上一層樓!

出處:https://www.finlab.tw/5-%e5%80%8b%e6%ad%a5%e9%a9%9f%e8%a8%ad%e5%ae%9a%e9%81%b8%e8%82%a1%e6%a2%9d%e4%bb%b6%ef%bc%8c%e8%82%a1%e7%a5%a8%e7%88%86%e7%99%bc%e5%8a%9b%e6%9b%b4%e4%b8%8a%e4%b8%80%e5%b1%a4%e6%a8%93%ef%bc%81/

策略公開,大部分都會無效化,唯有自己打造,永不失效

我們在新的選股平臺中,分享了一堆策略,但是這些是公開的策略,大家都看得到,所以難保之後不會失效。所以唯有打造自己的策略,你才更有底氣,在股票市場中生存!

設計股票策略,對剛開始進入股市,或是剛使用選股系統的人來說,並不簡單。我有看過初學者,花很多精力,直接爆寫長達 100 行程式碼選股,用了超多條件,但效果還是不太好。所以決定分享一個 SOP,只要遵照以下幾個步驟,通常都可以找到不錯的策略,量化交易不難,而設計策略其實可以很單純。

內容目錄 隱藏

1 1. 尋找因子

2 2. 判定效果

3 3. 決定是否棄用因子

4 4. 篩選條件

5 5. 重複 1~4 步驟

6 進階:非線性

7 進階:N 階因子

8 總結

1. 尋找因子

這邊說的因子,也不是什麼特別的東西,而是日常所使用的一些指標,可以是「股東權益報酬率」或是「RSI」指標,不論是技術面或是基本面,只要是可以量化的數值,都可以拿來當作因子。可以到我們的資料庫搜索看看,是否有你感興趣的資料。我們提供了大部分股票的資料,除了分點券商外(太貴還沒買),你想的到的因子都可以製作。目前(2021/11/21)完全免費,你可以趁這個時機全部下載下來試試看。

2. 判定效果

找到了想選股的資料,我們要先確定因子是否有效果,你要設計一個條件式,來測試單一因子是否有選股的效果。以「股東權益報酬率」來說明,我會設計從寬鬆到嚴苛,撰寫選股條件,範例如下:

from finlab import data
roe = data.get('fundamental_features:ROE稅後')

roe_rank = roe.rank(axis=1, pct=True)
stocks1 = roe_rank > 0.2 # 寬鬆,選擇前 80% 的股票
stocks2 = roe_rank > 0.4
stocks3 = roe_rank > 0.6
stocks4 = roe_rank > 0.8 # 嚴苛,選擇前 20% 的股票

我會將上述四個條件都試試,回測總報酬率或是夏普值,以程式碼中的 stocks4 為範例:

from finlab import backtest

report = backtest.sim(stocks4, resample="Q", fee_ratio=0, tax_ratio=0))

這邊的換股頻率是「每季 (Q)」,因為「股東權益報酬率」是每個季度會公佈,所以我們只關注每季價格的變化,但假如你的指標是價格,則可以使用其他的頻率,例如「每週 (W)」或是「每月 (M)」。有些人製作因子,也會用「近四季平均或累計」來表示,並且以「每年 (A)」為單位換股,這樣也是可以的。假如上述回測有長期打贏大盤,那可以說,這個因子可能是有用的!

但很多人看回測看的是「報酬率累計是否比大盤高」,這是不正確的做法,一個極端的例子就是,當一個策略報酬率如下:

  • 第一天比大盤多 3%
  • 之後每一天都跟大盤的報酬率一模一樣。

這樣此策略的「報酬率累加」永遠都比大盤的高,但實際交易並沒有顯著的效果。所以平常除了看 alpha beta sharpe 之外,也可以目測「報酬率累計跟大盤差距越來越大」,假如有的話,才代表此因子時時刻刻都在發揮作用。

3. 決定是否棄用因子

假如你發現此因子沒有任何作用(回測都比大盤爛),請你先直接棄用,而不是加入其他的因子。有點像是蓋大樓,假如地基沒有打穩,地震就會垮。你要設想 2000 檔股票裡面,其實只有幾檔是寶,假如今天用了差勁的因子不小心過濾移除了,錯過挑選它的好機會,有可能只選出一些烏合之眾,就算再用好的濾網,也選不出好股票。所以請直接將此因子移除吧!

4. 篩選條件

當做完上述檢測,你發現這個因子真的有用!就可以開始針對因子設定條件,你可以將第二步驟已經回測的數據拿出來,例如總報酬率如下(示意):

  1. 篩選出前 80% 的股票:總報酬 300%
  2. 篩選出前 60% 的股票:總報酬 500%
  3. 篩選出前 40% 的股票:總報酬 700%
  4. 篩選出前 20% 的股票:總報酬 700%

以上四種條件,從寬鬆到嚴格,你會使用哪一個條件呢?要記住我們的目的在於「總體報酬率高」及「有效分散風險」,前者當然是為了賺錢,而後者是為了「少賠錢」。有了以上得目的,相信你不難選出第三個:「篩選出前 40% 的股票」當作是最佳的條件了。你會發現 3 跟 4 篩選出的股票數量不同,但是報酬率都差不多,在同樣的報酬率下,越多股票對你越有利,除了分散風險外的好處,由於樣本數多,也不太會過擬合。還有額外的好處是:你保留足夠多的股票,可以再加入其他的因子來篩選出更有上漲潛力的股票。

5. 重複 1~4 步驟

假如你留有足夠多的股票,這時候就可以再加入因子,看看是否能提升報酬率。有些因子之間會有加成效果,而有些則沒有。你甚至可以調整因子篩選的順序,不過會建議主要的因子不要再改順序了,除非你想做一個完全不同的策略。至於到底有多少因子才夠呢?其實並不一定,但最終目的是篩選出 20~30 檔股票以內,畢竟太多股票對一般人來說也是很難一次買齊。

選擇較少股票,這其實是一件不容易的事情,因為股票少,整體投資組合勢必波動變大。因此在股票少的狀況下,必須確保策略報酬波動小,就是這個階段的關鍵。

新因子額外篩選股票,程式的寫法,如下:

stocks4 = (stocks4 * new_factor).is_largest(30)

其中 stocks4 就是原本篩選出來的股票,數值為 0 (False) 或 1 (True) 代表是否被選入,而 new_factor 就是新因子,這樣就可以用新的因子篩選出前 30 名的股票,這邊的 new_factor 是大於零,且越大越好。假如所有股票的 new_factor 同時有正負,要記得加上一個 threshold,讓 new_factor + th 的數值一定為正:

stocks1 = (stocks1 * (new_factor + th)).is_largest(10)

組合好就可以將篩選完的股票進行回測囉!

backtest.sim(stocks1, resample='Q')

以下還有一些複雜的方法,但是個人覺得用以上的方法找好策略已經綽綽有餘,除非你每一種因子都已經玩很無聊,那以下比較進階的方法就適合你:

進階:非線性

這個世界並不是線性的,以上述的範例「股東權益報酬率」來說,我們只是假設其數值越高越好,將因子與績效之間的關係假想成是線性。但現實生活中,報酬率與因子之間可能是非線性的,例如「股東權益報酬率」太高,有可能會有均值回歸的狀況發生,使未來股票走勢不好。假如希望可以做的更精細,可以在上述第四步驟中,再額外調整篩選股票的區間段,例如將股票依照因子大小分成五份來回測。

進階:N 階因子

另外,上述步驟中,是假設因子之間並沒有關連,但事實上它們之間會交互作用,例如你單純買「投信買進」的股票可能沒什麼用,但是假如你買「股價暴跌投信還買進」的股票,可能就有用。可以在上述第四步驟中,再額外加入這種反向因子,但是條件越多就會越複雜,要盡量避免過於複雜而導致過擬合喔!

總結

選股策略千百種,上述方法是我覺得一定可以產生出好策略的 SOP,但關鍵在於,不要盲目加入一堆條件,把好的股票都篩掉了!要慎選因子。現在就開始使用我們免費公開的超好用回測系統來選股吧!

順大逆小交易,哪種回調入場點勝率最高?

https://www.zhihu.com/question/309137888

入場方式 1.左側交易,提前預測進場點位最好,試錯率最高, 2突破進場,點位次之,試錯率比第一種低, 3突破回調進場,可能回調,也可能不回調,每人知道回調那個位置,試錯率最低,點位最差。

你要最高勝率就順大逆小,順大週期,逆次級週期建倉


勝率這東西本身就挺扯的其實,同等賠率下,單個的任意開倉訊號樣本越多越接近50%,只不過在某一時間段,根據行情波動節奏的不同會產生偏離,今年行情走的比較迎合這個訊號,那他可能勝率能達6成以上,明年行情進入不應期,勝率可能只有4成不到,當然 賠率越高 勝率的均值就越小。

同樣的,遇到支撐或者阻力 能有反應是常態,直接過去了沒反應是強,到不了是弱。只要它有反應,大機率是會產生開倉訊號的,但此時起的這波屬於反彈還是反轉,短期是否會有反覆,你事前終究無法預知,只能走一步看一步。

勝率的提高得靠綜合性主觀判斷來提高,任何機械性的簡單行為絕對無法把控勝率。它是事後統計的結果,你無法事前設定勝率,你只能事前設定賠率。(除非你本身就能決定價格或影響價格)

若你要執著勝率,多數時候是吃不到大行情的,行情初期有持續反覆是常態,為了勝率你通常不能承受浮盈回撤,只能提前處理單子 微利止盈或平保,只有少數極強的流暢行情才是你的主營菜。這雖也是一條路,但對心性和能力的要求更高。

若一定要給幾條提勝率的術法的話可以看下我之前寫過的專欄,根據多週期空間位置力量的全域評估來做單。早期寫的比較粗糙,只能算部分指導方針,但大致思路已呈現,懶得重新打字了。其實一個機會是否值得參與 進去後在看哪個週期能拿多久 都得依靠空間位置力量的大局觀,而何時開倉何時退出最重要的是靠對力量的理解。若要簡單粗暴的方法的話,順大逆小中 放大初期止損遠比研究開倉訊號本身來的有效,只要主勢不變 你就無需止損。而在短線中最簡單粗暴的就是降低賠率 再加單子的提前處理了,比如幽靈的時間止損等等,短線只追求流暢強勢行情,多角度凡見弱必出。


題主應該是新手,對於新手來說盡量從大週期開始做,這樣你做回調成功率比較高,如果你直接從小週期做回調,會被經常打止損,能從小週期養到大週期的單子很少,就算養大了,你的止損成本也很高了,可是性價比不高。小週期確實有節奏,但操作之前是需要嚴格選擇的,不是什麼行情都做的,很多人覺得既然是短線,那麼我就增加交易機會來彌補短線利潤不足的問題,然後就開始標準模糊化,看見跟標準相似的機會就做,最後就以頻繁打損結束。你可能會說,頻繁止損肯定是止損設置錯誤導致的,這種說法有一定道理,但是首先主要問題還是機會選擇的問題,好的機會是不會隨便打損的,不好的機會就會頻繁打損,你可以看看自己的歷史訂單,看看盈利的單子是不是很少會出現測試自己止損的現象,就算測試也是很少的次數,而不好的機會,會頻繁測試你的止損位。當然了,中長線的操作方式又不一樣,因為你有一個長期的目標,所以為了這個目標你肯定會被小幅度的多次止損,但不是頻繁止損,只要拿到了就是一條大魚,中長線以後再說。

日內這種就屬於小週期,你的目標不是切節奏,而是看壓力支撐,到位置就跑,或者有利潤就跑,因為你本身就是日內,所以你就不要想的太遠,吃碗裡就不要看鍋裡,喜歡YY是人的本性。

這樣操作之後,熟練了日內確實能賺錢,但是你會發現自己非常累,你不可能什麼也不幹,就天天盯著行情看,你還要生活和工作呢。

所以儘量選擇中長線,這樣自己也有時間來思考,等技術成熟了之後,再玩玩短線,看看自己是否適合。

 俗話說得好,會買股票是徒弟,會賣股票才是師傅。飆股人人都買過,但真正賺到大錢的百不得一。 by 奇正2

多重出場點:單純性與多個出場點  

交易系統設計應該採用單純的概念。我們之所以強調單純性,因為這代表相關系統是建構在「瞭解」的基礎上,而不是最佳化。  單純的概念可以引用到許多不同市場與不同交易工具。我們雖然強調單純,但交易系統仍然可以設定多個出場點。這是兩個不相互衝突的概念,單純性是交易系統之能夠有效的必要條件,多重出場點則是滿足交易目標的必要條件。出場點雖然有很多個,但每個出場點都可以源自簡單的概念。

讓我們看個例子。假定我們想使用順勢系統,而且希望留在市場久一點。我們不相信神奇的進場訊號,所以要留給部位較大的迴旋空間。另外,萬一出現重大不利走勢,系統必須保障資本,部位必須認賠。最後,由於起始停損相當寬鬆,我們將儘可能獲取較大的利潤,當獲利達到4R時,停止點將設定得更緊密一些。因此,我們要根據這些信念,設計一套適用的交易系統。這個例子顯示一項重要觀念:交易系統設計上必須符合個人信念。這也是交易系統設計的祕訣之一。  

首先,進場點的起始停損必須相當寬鬆,提供充分的迴旋空間,不至於造成訊號反覆而增添交易成本。我們決定採用前文提到的辦法:3倍的價格波動。這是最糟狀況的停損,但也是後續的追蹤型停止點,因為每天收盤價如果朝有利方向變動,我們將依此重新設定停止點。  

其次,我們相信,如果市場出現強勁的反向走勢,就應該結束部位。所以,我們決定,只要任何一天的價格反向走勢超過每天價格波動的2倍(由前一天收盤價起算),就結束部位。這個停止點與前一段的停止點是並存的。  

最後,獲利一旦到達4R,將採用緊密的停止點,避免吐回太多帳面獲利。所以,獲利到達4R之後,停止點將設定為平均真實區間的1.6倍(不是原來的3倍):從此之後,這也是唯一的停止點。  請注意,這些停止設定都很單純,清楚反映我們所想要的目的。沒有經過歷史測試,所以沒有最佳化的問題。完全沒有涉及火箭科學,所以很簡單。總共有3種停止點,但任何時刻都只有一個停止點真正有效,也就是最接近當時市場價格者。(摘自「交易‧創造自己的聖盃」/ 凡‧沙普)

我的多重出場點  

  1. 初始停損點:小於10%。  
  2. 特殊出場點:如果跌破重要支撐。  
  3. 追蹤型停止點:從高點(收盤)回落15%~30%則出場。  
  4. 緊密停利點:獲利一旦到達30%~100%,則採用緊密停止點--跌破ma5或ma20(不下移)*0.96。  
  5. 彈性原則:浩劫餘生與太空漫步,採取的策略當然有所不同。所以設計有一般波段模式與股災模式。

波段模式  

  1. 初始停損點為最大虧損 10%,越小越好但不能太小。  
  2. 由最高點回落 20%。  
  3. 不要求在期限內要漲多少。  
  4. 漲幅達到 30% ,改用 max(ma20)*0.96 為浮動停利點。

股災模式  

  1. 初始停損點為最大虧損 10%。  
  2. 由最高點回落 30%。  
  3. 不要求在期限內要漲多少。  
  4. 漲幅達到 100% ,改用 max(ma20)*0.96 為浮動停利點。 

虧損不超過10%  

「...限制虧損之後,雖然有些轉敗為勝的成功交易消失了,但這方面的不利影響,遠低於迅速認賠的有利影響。根據這項虛擬測試,整個投資組合的績效改善程度,實在顯著到到令人難以置信。我重複驗證相關的計算程序,數據都是正確的。我的投資組合表現,從原來的兩位數字虧損,變成了獲利超過百分之七十。」(摘自「超級績效--投資冠軍的操盤思維」/ Mark Minervini)

特殊出場點  

一般情形下原則上虧損不超過10%,但你可以設定的更緊密一點,然而你永遠無法逃過主力裝死、蹲伏、甩轎、洗盤的磨難,所以必須有所瞭解與對策。

交易儘量接近危險點  「...真正優秀的交易者,他知道如何判斷正常的價格拉回整理,以及具有危險的價格行為,他們會把停損設定在正常回檔即將演變為危險走勢的關鍵位置,然後讓交易進場價位儘量接近危險點。...」(摘自「超級績效2」)

蹲伏與反轉復甦  「...交易者建立部位之後,皆希望看到股票呈現應有的「行為」,但我們也想避免在不必要的情況下扼殺交易機會。行情沒有立即發動,並不代表相關交易就是失敗。...進場建立部位之後,不妨讓股票保有一、兩個禮拜的時間可以正常波動當然必須維持在停損範圍內。股票如果出線蹲伏情況,不必覺得恐慌,,只要停損沒有被引發,沒有出現主要違例現象,不妨等待看看股票是否會發生反轉復甦走勢。」(摘自「超級績效2」)

重新進場  「某些股票可能呈現理想的架構,吸引買盤進場,但走勢很快進行修正或急遽拉回,引發部位的起始停損。這種情況之所以發生,通常是因為大盤走勢轉弱或劇烈波動。一般來說,股票的基本面條件如果優異,價格向下修正或拉回之後,通常會再出現新的買點。這種新的買進架構往往較先前的買進架構更為優異,因為籌碼得到更一進一步的清洗。  

部位的起始停損遭到引發之後,不能預期該股票絕對會再出現買進架構;換言之,起始停損一旦遭到引發,就必須停損出場。反之,部位遭到停損之後,並不能排除相關股票再度成為買進對象的可能性;只要該股票滿足了所有買進條件,就可以重新進場建立部位,雖然時機可能相對落後。請注意,真正的重大獲利機會,經常是在重新進場兩、三次才得以掌握。可是,這點往往也是區別真正專業玩家的分野。業餘玩家可能被停損一、兩次之後,就不會考慮再進場,但專業玩家則永遠保持客觀立場,他們只考慮進場條件是否滿足,把每個潛在機會都視為全新投資進行評估。」(摘自「超級績效2」)  

頂尖交易員喬治.席格(George Segal)說:「我相信你想知道,我是如何得知應該何時進場的?在我真正建立部位前可能已出場二、三次(停損出場);有時我可以一次便成功地建立部位。但通常需要嘗試數次後,我才會覺得對盤。我願意隨情況的需要不斷試盤,這是一種獨特的觀念;多數人在市場中挫敗,因為他們只做一兩次的嘗試,往往在節骨眼上放棄。而我是不斷地回頭嘗試,不斷地敲著大門,直到大口敞開為止。」

為什麼不用其他出場法?  

 以臺積電為例,本文比較型態學出場法、均線出場法、基本面出場法、潛力股出場法。

使用形態學出場以臺積電為例。

[0066_臺積電漲倍圖]

img

下圖[0067_臺積電型態出場]顯示在2015年,使用型態學出場會被洗出兩次。

img

但這兩次的振盪在整個上漲中微不足道,可見得型態學並不適合長線操作。

使用均線出場

如果使用長線150日線作為出場條件,下圖顯示至少會被洗出三次。

[0068_臺積電均線出場]

img

均線眾所週知的缺點就是「太慢」,如下圖,等到出場已經吐回58%以上獲利。

[0069_國巨均線出場太慢]

img

由從可見,使用均線出場,不是太快就是太慢。

使用基本面出場

以下使用「超級績效--金融怪傑交易之道」/Mark Minervini舉的例子說明,散戶如果要依據公司的營運狀況變差來出場,那叫作「天方夜談」。

[0070_基本面出場太慢1]

img

[0071_基本面出場太慢2]

img

[0072_基本面出場太慢3]

img

[0073_基本面出場太慢4]

img使用潛力股投資法股災模式

下圖顯示使用股災模式出場法有不錯效果。

[0074_股災模式1]

img

[0075_股災模式2]

img

注意,只有在低檔,你認為有一倍以上的潛力時才使用「股災模式」,如果已經漲到第三波、第四波,就只能使用「波段模式」(預期有30%以上獲利),短線模式不要使用,你應該使用經過驗證過的「短線交易系統」,陰陽線、指標等不是經過驗證的系統,只不過是「見證」,見證與驗證不同,驗證是可複製的科學邏輯,見證大多隻是偶然。

[0076_股災模式3]

img

分批在操作前期的應用 建倉

  • 分批買進,只是建倉,壓低成本,抱得住後面才能暴賺
  • 美股要拆太多筆(10筆) ,可以向下分批
  • 美股震盪大15~20% 才加碼一次
  • 假設分三批進場,在三批全部打進去之前都視為建倉,所以在完成建倉之前,風險都控制在10元,所以在第二筆打進去之後,成本110應該是在105的時候就會止損了,"有賺不能賠"應該是建倉完畢後才開始執行
  • 前期建倉完成後是用一個沒有賠到錢心情在拼後面大利潤
  • 每後一次進場風險都變小
  • 暴賺哲學 選股標的同時持有不同股票

https://youtu.be/dS4VszQ0CbQ?t=428

QA:

如果我計劃性向下總共分3批買進,當我決定在100塊進場第一批資金,停損一般都設10%,其實我不太清楚我第二批的進場價位跟停損價位到底是要設在多少比較合理?
[假設第三批資金是等股價超過前兩批平均成本再進場。] 想請教老師下面哪種策略比較好?還是您建議如何改良? 

A. 93塊入場第二批資金,然後當價位跌到90塊時,第一批資金跟第二批資金一起停損出場,然後再等時機重新入場。 
B. 93塊入場第二批資金,然後當價位跌到90塊時,第一批資金先停損出場,第二批資金當跌到83.7塊時(-10%)才停損出場。然後再等時機重新入場。 
C. 第一跟第二批資金平均成本96.5,當股價跌到86.9(-10%),兩批資金一起停損出場。然後再等待時機重新入場。

我選股是以基本面為主,籌碼面為輔,技術分析只看簡單K線而已,因為我不是用技術分析找買點,通常決定進場價位是等股價相對跌一大段進場。
A.平均成本10%是偏純策略的做法
B.90全出是技術分析關鍵點的做法
C.83.7再出的做法算是有點混合技術+策略的做法

拉回加碼!

獨孤求敗, 加碼, 進場點, 移動停利, 風險控制, 臺指期操作, WINSMART, 期貨投資教學

今天要講的主題是【拉回加碼】,買在比較好的進場點。我們從下面的 3 個主題來分享:

  1. 如何大賺一筆
  2. 為什麼要加碼
  3. 怎麼加碼在好的點

如何大賺一筆?

我們先來看看如何大賺一票,那麼要大賺一票不是說要下【重注】來去大幹一票,下重注的話如果你看錯也是【大賠一票】。

我們應該應該要怎麼做呢?我們應該要【控制部位】也就是說持有【合理的部位】,然後呢合理的部位如果你做短線那麼你沒有辦法賺到太多的錢。你要怎麼做呢?你應該要【拉長線】才有辦法賺到最多的錢。

【拉長線】才有辦法賺到最多的錢

也就是說,行情出去的時候你要【抱好】你的部位,你可以靠大的價差來賺到財富,大賺一票的思維就是這樣子。

如何抱一段?

那如何【抱一段】呢?我們都知道行情會上上下下跑來跑去,我們可能行情一拉回就出場了,要怎麼抱一段呢? 我的答案是:「允許比較大的價格回檔」你就可以抱一段」。

我們可以看一下下面的圖,藍色的線就是你【移動停利】的軌跡,沒有跌破藍色的線你就不用出場。如果你的藍色線貼價格貼的非常近,那麼價格拉回 40 點 50 點就出場,那你就不可能抱一段。

過渡交易,不守風險=畢業

你必須要能夠接受比較大的【震蕩】的【回檔】,至少接受震蕩 150 點以上你才比較有機會可以抱一個波段,而且常常在行情大的時候你可能要把震蕩提升到 200 點甚至 300 點、600 點都有可能。

那麼什麼時候會提升到 600 點以上?就是在極端行情發生的時候,例如:2020 崩盤 3 月的時候震蕩的幅度非常非常的大,一天的振幅就有 600 點,那你至少要波段拉大你才有可能抱得住部位,如果你還是在 100 點 200 點的話,你不可能抱得住一段【行情急殺】的大波段行情。

所以,你要看行情幅度大的話,你要忍受比較大的回檔,才能夠抱住這一段大的行情。如果你一拉回一點點就跑掉的話,那你就不可能抱一個波段的。

就是,你要【耐震】。

為什麼要加碼?

其實答案就是「加碼賺更多」啦。

加碼賺更多

我在講這個主題的時候,曾經有一個同學問我:「如果都已經知道行情會上漲了,為什麼不一開始就買完了,還要中途去打加碼?後面成本是越來越差耶?」。

對,「我們就是不知道行情會走那麼遠,才需要在過程中去放大我們的部位,去打加碼」,而這個放大部位的話,我們可以怎麼樣?我們可以【計算風險】。

我們可以計算你買 4 碼買 5 碼,你買 6 碼,你的風險都是非常非常小的,這是可以透過風險來計算的,透過進場點、出場點來去做計算的,絕對是可以計算的。

例如:你打到最後一碼的時候,你可以決定說「我打到前一碼的進場價,就全部出場」,那如果是這樣子的話你前面的加碼都是賺錢的,你只賠最後一碼,那你還是賺錢的嘛。所以,加碼以後還是可以讓你安全的出場,這個是做得到的,你只需要【精算出場點,精算風險】就可以。

所以,這個賺錢加碼賺更多的邏輯就是,我們一開始用【最小】的部位去試單,行情出去的時候你要【大賺一票】,你就要敢【加碼】

違反人性的操作

其實這個【加碼】你就是跟一般的投資人做反向的,因為一般投資人是行情有賺錢就會想要走,獲利回吐就會想要出,這個就是【人性】。

一般人不是賺夠想走就是獲利回吐馬上想出,怎麼會想到要加碼呢?所以這是一個完全【違反人性】的操作方式,因為加碼代表:別人想出場,你想的是進場,如果行情出去的話,你就有可能賺到最多的錢。

因為違反人性,所以大部分的人做不到。

為什麼一加碼就停損?

有同學問:老師,我也想加碼,但是一加碼就停損。。。

我的答案是這樣:創新高加碼的勝算不高,除非趨勢盤不拉回。

創新高加碼的勝算不高,除非趨勢盤不拉回

例如上圖,行情上漲過程中,不會走直線上漲的,它會上去下來上去下來刷來刷去的。

如果你在行情出去的時候去【追價】,追價以後設定一個停損點,後面行情掉下來就可能打到你的停損,打到你的停損後,行情又可能又上去。這種情況就是行情在強勢的時候,追高的時候你去做進場那你就可能打到停損後行情又上去了,有時候對有時候錯,所以追高加碼勝算其實不高應該是低於 5 成,可能是 3 成

所以如果你使用追高加碼,是非常有可能常常打到停損的。你想想看你的停損可能設 50 點,進場後行情拉會 100 一定掃到你的停損嘛。

如何拉回加碼?

ATR 拉回 0.5 以上

我們去看一天的波動的高低差到底是多大,如果一天的波動是 160 點那麼我建議你,不要去追高。你等行情掉下來 0.5 ATR 的時候你再去做進場,建議是這樣子。

要去追高。你等行情掉下來 0.5 ATR 的時候你再去做進場

當你不追高,在拉回的時候進場。這樣你就不會買在差的點,這個前提是要在多方的趨勢盤,當行情正常回檔(次級回檔)的時候,你來做加碼你可以買在好的位置好的點位。

你要有耐心的等待行情的拉回。因為趨勢盤漲個 1 天、2天、3天、5天、7天、8天、10 天,它不會直線漲上去的,如果是當天短線趨勢盤,它可能直線上去,因為當天非常強,如果是長線的趨勢盤,例如臺指期 8 天的趨勢盤,就不可能每天都往上衝,一定在中間會有價格回檔的時間點,這個價格回檔的時間點你一定可以【找到更好的位置切入】。

拉回買,還不夠

我們可以有另一種方式進場,如果行情拉回 1 倍 ATR 那我們不是買在半路上嗎?我們可不可以跌完再進場?當然可以!

看行情形態的人可以在行情跌完以後往上打勾(紅色箭頭)的地方來去做加碼,也就是說最好買在行情的右邊。

最好買在行情的右邊

KD 低檔黃金交叉

我們可以用許多的技術指標來確認打勾,例如:KD 黃金交叉,KD 在低檔黃金交叉可以買在右邊打勾的位置。如果行情拉回幅度夠大,同時搭配 KD 在低檔黃金交叉的時候,你可以把這個交叉當成參考之一在底下做一個買進的動作。

那麼什麼叫低檔? 50 以下叫低檔,你也可以說 30、25 以下叫做低檔黃金交叉。

你可以把這個交叉當成參考之一在底下做一個買進的動作

交易範例

我們實際來看一下【拉回加碼】,範例中,WINSMART 幾乎可以幫到你完成整個交易。

用 WINSMART 幫你掌握拉回加碼的交易技巧,絕佳進場時機 !

行情回檔是停利出場還是拉回加碼呢 ? 要如何定義拉回加碼或是反彈空呢 ? 可以用什麼技術指標來定義呢 ? 讓獨大教你掌握拉回買反彈空的技巧,是進場加碼的絕佳時機 ! 讓你避開追高殺低,買在行情噴出前 !


JG 建倉技巧

統計過 20% 之後會反彈勝率 90% 這個有待商榷 因為有些漲多會跌40~50%

初始條件:

  • 初始本金: 100 元
  • 最高價格: 100 元

購買策略:

  • 股價下跌 15% 時,進行第一次購買。
  • 每次購買金額固定為 25 元。
  • 每次下跌 5%,進行一次購買。
  • 購買直到股價下跌 30%。

購買點位:

  • 第一次購買(跌 15%):100 元 * (1 - 15%) = 85 元
  • 第二次購買(跌 20%):100 元 * (1 - 20%) = 80 元
  • 第三次購買(跌 25%):100 元 * (1 - 25%) = 75 元
  • 第四次購買(跌 30%):100 元 * (1 - 30%) = 70 元

每次購買的平均成本:

  • 平均成本 = (85 + 80 + 75 + 70) / 4 = 77.5 元

停損點位:

  • 設定停損點為 31%
  • 停損後的本金:100 元 * (1 - 31%) = 69 元

虧損百分比:

  • 虧損百分比 = (77.5 - 69) / 77.5 * 100 ≈ 10.97%

PTT

我來幫你整理這些網友對股票加碼策略的建議:

  1. 加碼金額配置原則:
  • 第一次試單金額較小(總資金1-2成)作為試探
  • 第一次加碼比例要最高(約3-4成資金)
  • 後續加碼金額要逐漸縮小
  • 建議總資金分3份進場,第一二筆要投入50%以上
  • 加碼後的部位建議小於或等於原部位
  1. 加碼時機點選擇:
  • 等股價拉開一定距離後再加碼
  • 看5日線、10日線不破才加碼
  • 在回測有支撐時加碼
  • 區間突破的第一根K線可以加碼
  • N字型走法跌回10日線和20日線時
  • 大量低點可以慢慢建倉
  • 只在回檔時加碼,不追漲
  1. 風險控制:
  • 先設好停損點再考慮加碼
  • 用前次加碼點設為停損點
  • 走勢不如預期要快速減碼或退場
  • 確認大盤和類股是否處於有利走勢
  • 買在「舒服的位置」,確保停損範圍小
  1. 重要觀念:
  • 第一次買進的獲利要能cover後續加碼的風險
  • 加碼後要保留4-5成資金以應對後續變化
  • 要確認是處於「右側」走勢
  • 要考慮大盤走勢和產業類股資金動向
  • 只加碼有把握的型態

這些建議強調要謹慎加碼、注意風險管理,並且要配合大盤走勢來操作。主要觀念是越買越少,而不是越漲越加碼,這樣才能控制好整體風險。

當沖交易心法與技巧整理 (阿魯米)

當沖基本原則

選股條件

  1. 選擇平均震幅大的股票
    • 近三日或五日平均震幅高於 5%
    • 避免選擇震幅小的股票(如中華電信)
  2. 周轉率高的股票
    • 代表流動性好
    • 大量但周轉率低的股票(如台積電)不適合
  3. 日線處於趨勢發動中或高檔震盪的股票
  4. 年輕、股本小且震盪大的股票
    • 新上市櫃十年內
    • 法人持股不多
    • 容易有大波段

交易基本概念

  1. 觀察大盤方向

    • 大盤是最主要的方向參考
    • 參考期貨,盡量不逆勢操作
    • 台指偏多則選多方名單,空方亦然
  2. 開盤價的重要性

    • 不要只看漲跌,要看開盤價位置
    • 在開盤價附近區間脫離一段,基本上就是當天方向
    • 開盤價可視為多空分界
    • 價格在開盤價以下表示當天弱勢,收黑機率大
    • 價格持續在開盤價上方,表示強勢,收紅機率大
  3. 均價線運用

    • 均價線代表當天成交平均價格
    • 可視為短期均線概念
    • 在均價線上方逢低接
    • 在均價線下方逢高空
  4. 盤中量能觀察

    • 一個方向走一段時間出現大量,通常是極短線高低點
    • 可能出現拉回或反彈(但不代表反轉)
    • 出量位置若不拉回或反彈,則成為支撐壓力區
    • 上漲出量、拉回量縮是多方表現
    • 下跌出量、反彈量縮是空方表現

風險控制

停損原則

  1. 停損、停損、停損(重要所以說三次)
  2. 收盤前清理虧損部位,不帶回家
  3. 每筆交易虧損控制在 2% 以內
  4. 遇到重大虧損應:
    • 立即減少交易量或停止交易
    • 重點是重拾交易信心,而非急於彌補虧損
    • 避免單量越做越大想一舉翻身

心態管理

  1. 保持平靜

    • 市場隨時可能重擊
    • 虧損時表示情況不利,不要急躁
    • 大賺大賠都要保持冷靜
  2. 避免衝動交易

    • 最糟糕的交易來自衝動
    • 根據既定信號進行交易
    • 不要因一時衝動改變策略
  3. 持續學習檢討

    • 每天分析每筆交易
    • 檢討是否有違規情況
    • 分析成功與失敗原因

進階技巧

季線理論

  1. 股價在季線(60日均線)上徘徊一段時間後
  2. 開盤價直接開在季線下至少 2%
  3. 通常代表空方力量強大
  4. 適合當沖做空
  5. 觀察 09:30 前是否拉過平盤
  6. 勝率約九成

交易時機選擇

  1. 等待市場明朗再進場
  2. 不預測行情,讓市場告訴方向
  3. 選擇萬無一失的機會
  4. 策略需具彈性應對市場變化

資金管理

  1. 操作順利時可適度加碼
  2. 情況不佳時減碼或停止交易
  3. 著重如何減少虧損,而非如何多賺
  4. 避免重倉操作
  5. 好倉要耐心持有,壞倉要果斷減碼

市場認知

交易本質

  1. 交易是一種對賭遊戲
  2. 競爭對手是整體市場參與者
  3. 獲利來自對手的錯誤決策
  4. 重要的是認清自己,發現並改進錯誤
  5. 遵守紀律,提高勝率

市場參與者

  1. 不要高估對手能力
  2. 也不要輕視對手
  3. 資訊不對稱是主要優勢
  4. 主力也會演戲誤導市場

長期生存之道

  1. 穩定獲利比單次暴賺重要
  2. 交易系統要持之以恆
  3. 賺錢時要更加謹慎
  4. 重點不是瞬間獲利多寡
  5. 而是能賺得長,活得久

實戰注意事項

  1. 當沖很難穩定獲利

    • 交易成本高
    • 隨機性強
    • 需要長期練習
  2. 不要相信誇大廣告

    • 特別是號稱暴賺
    • 無本操作
    • 華麗詞彙包裝
  3. 新手建議

    • 不要期待暴賺
    • 先求穩定獲利
    • 逐步提升層次
    • 從基本心法做起
  4. 盈虧比例

    • 當沖輸的人多,贏的少
    • 穩定賺錢者極少
    • 要思考自己的優勢在哪

注意:本文整理僅供參考,投資一定有風險,須依個人判斷為準。


  • 做空
    • 空頭年(2022年) 找弱勢股拉高隔天空 by 權證小哥
    • 原本基本面很好 突然不好用股期做空 by 非比斯

import os
import json
from loguru import logger
import pandas as pd
import shioaji as sj
import matplotlib.pyplot as plt
from matplotlib.dates import DateFormatter


def load_credentials(file_path):
    """Load API credentials from a JSON file."""
    with open(file_path, "r") as f:
        users = json.load(f)
    return users["Jason"]["api_key"], users["Jason"]["secret_key"]


def login(simulation=False):
    """Login to the Shioaji API."""
    api = sj.Shioaji(simulation=simulation)
    token_file = os.path.expanduser("~/.mybin/shioaji_tokens.json")
    api_key, secret_key = load_credentials(token_file)
    api.login(api_key, secret_key, contracts_timeout=30000)
    return api


def detect_pullback(tick_data, threshold=0.02, lookback=10):
    """
    偵測符合條件的型態:
    1. 低點必須在高點左邊
    2. 高點與低點的差距必須超過 threshold (預設 2%)
    3. 當價格從高點拉回後觸發
    :param tick_data: 含有 'ts' 和 'close' 的 DataFrame
    :param threshold: 高低點差距的百分比門檻 (預設 2%)
    :param lookback: 在多少範圍內尋找高點與低點 (預設 10 個 tick)
    :return: 回傳一個字典,包含觸發與否和相關時間
    """
    for i in range(lookback, len(tick_data)):
        window = tick_data.iloc[i - lookback : i]
        low_idx = window["close"].idxmin()
        high_idx = window["close"].idxmax()
        if low_idx < high_idx:
            price_change = (
                tick_data.loc[high_idx, "close"] - tick_data.loc[low_idx, "close"]
            ) / tick_data.loc[low_idx, "close"]
            if (
                price_change >= threshold
                and tick_data.iloc[i]["close"] < tick_data.loc[high_idx, "close"]
            ):
                return {
                    "triggered": True,
                    "low_time": low_idx,
                    "high_time": high_idx,
                    "pullback_time": tick_data.index[i],
                }
    return {"triggered": False}


def print_usage(api):
    """Print API usage statistics."""
    usage_status = api.usage()
    usage_MB = usage_status.bytes / (1024 * 1024)
    limit_MB = usage_status.limit_bytes / (1024 * 1024)
    remaining_MB = usage_status.remaining_bytes / (1024 * 1024)
    logger.info(
        f"connections={usage_status.connections}, "
        f"usage MB={usage_MB:.2f}, "
        f"limit MB={limit_MB:.2f}, "
        f"remaining MB={remaining_MB:.2f}"
    )


def fetch_and_process_ticks(api, stock_code, date):
    """Fetch and process tick data for a given stock and date."""
    contract = api.Contracts.Stocks[stock_code]
    ticks = api.ticks(contract=contract, date=date)
    df_ticks = pd.DataFrame({**ticks})
    df_ticks["ts"] = pd.to_datetime(df_ticks["ts"])
    df_ticks = df_ticks.set_index("ts")
    return df_ticks[~df_ticks.index.duplicated(keep="last")]


def plot_ticks_with_markers(df_ticks, result):
    """Plot tick data with markers for high, low, and pullback points."""
    plt.figure(figsize=(12, 6))
    plt.plot(df_ticks.index, df_ticks["close"], label="Close Price")

    if result["triggered"]:
        plt.scatter(
            result["low_time"],
            df_ticks.loc[result["low_time"], "close"],
            color="green",
            s=100,
            marker="^",
            label="Low Point",
        )
        plt.scatter(
            result["high_time"],
            df_ticks.loc[result["high_time"], "close"],
            color="red",
            s=100,
            marker="v",
            label="High Point",
        )
        plt.scatter(
            result["pullback_time"],
            df_ticks.loc[result["pullback_time"], "close"],
            color="blue",
            s=100,
            marker="o",
            label="Pullback Point",
        )

    plt.xlabel("Time")
    plt.ylabel("Price")
    plt.title("Tick Data with Pullback Detection")
    plt.legend()

    # Format x-axis to show time
    plt.gca().xaxis.set_major_formatter(DateFormatter("%H:%M:%S"))
    plt.gcf().autofmt_xdate()  # Rotate and align the tick labels

    plt.grid(True)
    plt.tight_layout()
    plt.show()


def main():
    api = login()
    try:
        df_ticks = fetch_and_process_ticks(api, "1316", "2024-02-27")
        print(df_ticks.to_markdown(floatfmt=".2f"))
        result = detect_pullback(df_ticks)
        print(result)
        print_usage(api)

        # Plot the tick data with markers
        plot_ticks_with_markers(df_ticks, result)
    except Exception as e:
        logger.exception(e)
    finally:
        api.logout()


if __name__ == "__main__":
    main()

`

當沖放空策略:

選股條件:

  1. 當沖週轉率排名前100,且最近三天內(不含當天)有過漲停的股票。
  2. 五日均線與現價比值大於0.85(五日均線上揚)
  3. 當日成交金額超過3,000萬。

進場條件:

  1. 符合條件的股票,在當日上漲6%時進行放空,設置9%停損,若未達9%停損則抱到尾盤出場。
  2. 當天開盤為長紅棒,但不可接近漲停,避免過強的上漲力道。
  • 勝率約60%
  • 盈虧比:
    • 最大損失:3%(由上漲6%至9%停損)。
    • 最大獲利:約15-16%(由上漲6%至下跌-10%)。

選股條件

  1. 當沖賺錢 TOP 100 股票:這些股票可能會因為賺錢而有人選擇賣出。
  2. 當沖虧錢 TOP 50 股票:這些股票可能會因為虧錢而有人選擇賣出。

日K 篩選條件

  1. 三日內有漲停或大漲

    • 三天內(不包括當天)曾經漲停或出現一波漲幅(至少 5% 以上)。
    • 三天內有漲停紀錄的股票更適合做空,因為上漲後可能有回落壓力。
  2. 當日漲幅與 K 線條件

    • 當日不能漲停,因為過強的股票做空風險高。
    • 當日漲幅超過 5% 且為長紅棒(強勁上漲)。
  3. 移動平均線條件

    • 5 日均線(MA5)相對於當日收盤價的比例大於 0.8 或 0.85。
  4. 當沖排行觀察

    • 根據權證小哥的觀察,當天當沖賺錢或虧錢的個股,隔天下跌的機率較高。

操作策略

  1. 隔天空方操作

    • 第二天開盤後,如果漲幅超過 3%,可以選擇放空,並於收盤時回補。
    • 在股票接近漲停前應設定停損。
  2. 細部策略調整

    • 若開盤上漲 3%,且股價進一步上漲至 5% 或 6%,進行放空。
    • 若開盤價格太高且接近漲停,不建議放空(因為強勢股反彈的機率大)。
    • 若開低走高,等股價到達高點(約 6%)時進行放空,通常主力不會一直推高,反而會出現回檔。

總結

此選股策略著重於選擇近期曾經有強勁表現的股票,並利用當天的漲幅與市場情緒進行反向操作(放空),適合在高點做空的交易者。此外,根據當沖排行數據進行觀察和操作,可以提高操作的成功率。


台股冷知識 熱門股每天開高機率是開低的兩倍,且幅度平均值差不多,換句話說, 1.多單凹單贏的機率比空單大很多。 2.隔日沖長期只適合做多

台股冷知識(2) 熱門股每天日內走高跟走低的比率大概是3:7,且平均幅度差不多,換句話說: 1.當沖放空會比做多容易的多 2.當沖做多賺錢的人多半會留倉,因為凹單相對容易解套

台股冷知識(3) 熱門股下殺V轉的轉折點有50%機率出現在 #九點十五分到十點十五分之間,其他50%機率隨機分散在剩下的三個半小時加總,換句話說 抄底只能在10:15之前,10:30之後破底則多單停損會比凹單來的好

台股冷知識(4) 出掉庫存可以指定沖銷,一般來說系統都是採用先進先出,但是以前當營業員的時候會有些人為了已實現損益比較好看,會把虧錢的留倉,然後指定沖銷賺錢的單,這種改帳的人可以分成兩類 1.欺騙自己,沒賣就不算賠 2.欺騙會員學生,老師每天當沖都賺錢

台股冷知識(5) 熱門股拉到最高點後A轉的時間點有70%機率出現在 #十點十五分前,其他三個半小時出現最高點的機率不到30%,換句話說,日內放空進場點在10:15之前比較好。

台股冷知識(6) #交易賺很多的人其實常常看不準行情,贏家跟其他人的差別在於行情變化時如何調整部位,透過資金控管達到大賺、小賺或小賠,時間拿來思考行情看對怎麼做,而看錯又怎麼應對會比預測行情更有效益。

台股冷知識(7) 台股籌碼透明度其實蠻高的,櫃買中心甚至揭露 #盤中每小時熱門股券商分點的進出,盤中可以看到前一天買的人走了沒、今天誰買了,真的是很佛心,

https://www.tpex.org.tw/zh-tw/mainboard/trading/realtime/broker-vol.html?fbclid=IwZXh0bgNhZW0CMTAAAR1SlCxQyk6OWTRMezvTVlydsAPtj1k9YOb4YFn6kFUVU3BPzj7jplLU-Sc_aem_cCu9O2aSUa7JhjJlhO5WMQ

台股冷知識(9) 日內當沖分批停利其實會減少獲利 日內最佳停利點就是跌停附近,下殺分批停利只會減少獲利,少數情況適合分批停利: 1.#部位很大,考慮到流動性問題需要分批出場 2.#信心薄弱,先拿獲利起來會讓心理上舒服一點 3.#背離過大,可能反彈所以先補,但轉折點極難判斷,很容易錯過低點

台股冷知識(10) 日內最佳出場時間在12點之後 跟冷知識(9)相呼應,但(10)同時適用於停損及停利,換句話說,未觸及漲停、跌停前,提早出場只會降低交易的期望值

台股冷知識(11) 日內當沖停損點越遠越好 很多人對於如何停損都是一知半解,首先要定義 #停損的目的是要減少虧損,一般做法有兩個 1.風控 當天虧損不能超過資金比例的1%或2%,很多法人喜歡用類似停損方式,背後的含義大家應該都懂。 2.期望值 找出哪些關鍵價位進出場可以增加獲利或是減少虧損,透過回測可以發現其實日內不要停損最好,但其實這結論是錯的,因為還要考慮 #被鎖漲停隔天開高的虧損,考量進去後就是我採用的方法 寫這篇的用意在於很多人會留言或私訊為什麼那個價位我沒停損,我只能說背後有相關數據支持,適合我(別人)的不見得適合你,找出適合自己的策略比較重要

台股冷知識(12) 盤中觸及漲停的股票,隔天容易開高 鎖漲停的股票隔天開高大家都知道,有趣的現象是,#沒鎖的股票隔天也容易開高或拉高

台股冷知識(13) 日內行情殺3%以上出現的次數一個月不到2次 換句話說,這個月殺最爛的日子大概就是今天了

抄底看什麼

1、價格乖離 可以統計一下該股的乖離,看看多少算大,懶得算可以看布林通道邊界,但是布林通道是以20日平均來計算,我是會拉長一點看,量看看過去大乖離的紀錄。

2、價量型態 型態我覺得是市場心理的一種模式,日本人觀察到市場價量有一定的模式,紀錄後應用在交易上,後來被美國人帶到西方世界發揚光大。型態不是隨時都有用,很多人對型態最大的誤解,就是認為「型態隨時隨地都能解釋價量行為」,不是這樣的,股票更多時候反應的是隨機行為,型態只有在關鍵時刻比較有參考性,但「那也只是參考」,所謂的參考是「劇本」,但實際上交易要「符合劇本」才能做,因為一旦符合劇本出現,市場的氣氛和心理就越會依照劇本而行,因為行為都是被經驗所強化而驅動。舉個例子:你每次出門都去上廁所,保證3個月後出門前你一定尿意就來。所以抄底的時候,我會特別看一下型態,是否有「竭盡」的感覺。

3、技術指標 透過觀察與名師教導,融資維持率是抄底時最好要看一下的數據,甚至可以提前去預估「跌到哪裡會斷頭」。從數據上,我們也會發現自營商在吃客戶的豆腐,自營商也很愛撈斷頭股。這算是資訊上的優勢?

4、趨勢格局 主力搶反彈也是會看一下大格局的氣氛,沒有人會在明知美股高機率崩盤前去發動抄底。或者是即將發生空方大事件前進去抄,所謂的空方大事件,並不是指已經發生的事件,而是「未來可能發生對多方不利的事件」,市場最不喜歡的事件是什麼?答案是「未知的空方事件」,市場最不愛不確定性。但是未知的多方事件則不一樣,反而常常帶來默默的偷漲。所以搶反彈也要去估計一下,美股是否也穩定?台股是否出現竭盡?想一下後幾天的劇本,讓趨勢有利於主力發揮。

5、籌碼追蹤 我是比較少看籌碼,大部分都只看三大法人+融資券,籌碼少看是因為太累,而且未來希望偏向大股票操作,少做冷門內線股,所以就越來越不愛看籌碼。但是抄底的時候,會稍微看一下整個族群的大人都在幹啥?如果大人給我的感覺是越賣越少或者意外逆勢進場,很可能隔天8:45一開始就看期貨的開盤先打開盤sop,如果真不行再盡量保本出,重新找v轉位置再打(最愛假跌破)。

6、賺賠設定 通常進場前會用支撐壓力找第一目標做減碼,比較常用的是等幅的亞當反轉。支撐壓力看一下線型和大量成交去猜,下行階段的抄底請不要太貪心,要設定天數和幅度兩個濾網,但如果是上行週期抄底,就會比較貪心,第一目標減碼後就放他跑看看。跟以前不同之處,是以前我會突破後小平台整理加碼,但現在都是一次買滿,連多買的都算好,往上只會減碼絕對不加碼。想一次加夠,得鍛鍊自己的心理肌肉和紀律,慢慢把自己的部位一次拉大,壞處是遇到飆股總會覺得沒賣真好,幹嘛減碼呢?一直靠北自己。如果心臟很小顆很難克服放大部位的恐懼,還是慢慢加碼上去比較適合。我現在心臟也不夠大,但是就是敦促自己慢慢增加啦,理論上我個性是慢慢加碼舒服的那種,但已經被刻意訓練可以一次尻大一點,雖然對很多人來說還是不大啦。這個真的無解,只能慢慢刻意鍛鍊心理的肌肉。

麻道明

現在市場中各熱門題材持續火熱,這時候再多的選股策略似乎顯得累贅, 想到國外有一本書叫做《賺贏大盤的動能投資法》, 書中講的贏家策略只有一個,就是~買進價格上漲的股票!!

書中闡述的原理是,動能效應在股票市場上不可能消失,因為在人性的驅使下,贏家總能循著正確的足跡前進。

所以呢,我們這次利用XQ全球贏家的【量化積木模組】,鎖定周轉率、成交量(值)較高的股票,且大盤在多頭行情底下(大盤在5日均線之上),當行情正在發動的時候(盤中開高3%),動能投資法就會選擇進場參與,所以會享受到較大的價格波動。相關設定與績效如下圖:

img

img

績效圖如書中所述,真的可以打敗大盤呢!!

動能投資法有兩個好處: 1、減少資金閒置時間成本: 2、果斷停損:動能投資法以股價的動能作為進出場訊號,當股價失去上漲的動能,就果斷出場。不像價值投資法需要等待基本面修正,那可能要等很久呢!

然而,動能投資法也有兩個缺點:相比起價值投資,動能投資法更能利用資金,因為我們不停買進有漲勢的股票,賣出沒動力的股票,讓資金一直跟著行情動盪,不讓它閒著發黴。 1、高周轉率增加交易成本:動能投資法的特點是頻繁進出市場,那交易成本也就提高了。相比起價值投資那種長期持有,我們得更頻繁地操作,這是它的必要之惡啊。 2、進場時機較晚:動能投資法不像價值投資法那樣會看基本面估值,而是等到盤面顯示進場訊號才進場。所以我們進場的時機往往相對晚,可能會買在股價基期較高的位置,也就是「買貴賣更貴」的意思。

在使用動能投資法時,別忘了這兩個缺點哦。要謹慎管理交易次數,避免頻繁進出帶來的成本增加。同時,可以嘗試結合基本面估值分析,以降低進場基期較高的風險。

瞭解動能投資法的優缺點有助於更明智地運用這個策略。根據自己的風險承受能力和投資目標,選擇合適的投資方法,同時採用適當的風控策略,才能獲得穩定的投資回報。

以上分享,希望對各位有幫助。


動能投資法是一本實戰策略書,雖然裡面的策略看起來應該算是初步策略,策略建立該思考的地方都講得非常完善,很值得發一篇文章來討論這本書的策略寫法。也希望能對正在學習建立策略的人有幫助。

動能投資法策略細談

1.空頭市場不買進股票(SP500跌破200 MA),增持現金部位。

2.股票排序以90日股價為基準,利用指數回歸計算出每天漲跌幅,再換算成YoY。(圖1)

圖1、線性回歸作法

圖1、線性回歸作法

3.排序會剔除跳空走勢,提出方法是利用勢配率(R^2)來看指數回歸的相關性,R^2越小,就代表指數迴歸結果參考性越低。

4.為了兼具動量與品質,最終排序方式使用指數回歸斜率YoY*R^2,進行排序,符合策略的股票,計算結果也越高。(圖2)

圖2、策略指標示意圖

圖2、策略指標示意圖

5.買進部位使用波動率作為計算基準,波動率指標採用ATR,當20日平均ATR越高,買進部位越低,盡可能維持每一檔標的每日的指數波動相近,平滑波動性。

6.策略將從標普500挑選,不限制挑選出來的股票族群佔比,只以排序為主,並從排序最高的個股一路買,直到用盡現金。

7.為增加成功率,外卡兩道濾網,包含個股股價要高於100 MA,以及90天內沒有出現超過15%以上跳空缺口。

8.買進成分股後,不會設定停損點,而是利用汰弱留強的方式,每週固定一天檢視新的SP500成分股排序,若個股排序低於前20%,或是股價跌破100 MA,就全數出場。

9.除了檢視機制,投資組合也會定期進行再平衡,每隔兩周的檢視日進行調整,利用新的ATR與收盤價重新計算。(圖3)

圖3、策略流程圖

圖3、策略流程圖

策略實測結果

策略從1999年測試到2014年,每一年都會分別話說SP500 VS 策略的績效,以及持有現金比例。同時也會檢討當年度操作比較有代表性的個股,很適合讀者一步一步觀察策略實際實行後會發生的問題:(圖4)

圖4、1999~2014年逐月績效

圖4、1999~2014年逐月績效

1.在多頭時期,是策略最穩定的狀況,當SP500一直處在200 MA之上時,就代表策略可以不斷運行,藉由每週更新的策略清單換股,以及每兩周依照ATR進行部位調控。在這個階段,很容易就得到比指數更好的報酬率,也達到策略設計的目標。(圖5)

圖5、多頭行情當年報酬圖

圖5、多頭行情當年報酬圖

2.在空頭時期,是策略持有現金最高的時期,當SP500沒有突破200 MA,策略就不會買進股票,當成分股跌破100 MA就會執行出場策略。經由實測,虧損可以非常有效的控制住,績效也顯著勝過SP500。然而,缺點是長時間都不會執行策略,這樣的空洞期不太容易忍住,也會產生質疑策略的心態。(圖6)

圖6、空頭行情當年報酬圖

圖6、空頭行情當年報酬圖

3.策略在高波動時期,績效表現最為震盪。動能投資最怕的就是在判斷基準線上來來回回,低於基準線就要出場,高於基準線又要入場。這樣反覆來回很容易出現買高賣低的情況,績效會非常容易落後大盤。作者認為不應該根據事後的狀況改變參數設定。但我認為,應該思考該如何判斷目前是高波動盤,同時該如何保留獲利,避免不斷的買高賣低。以增加策略靈活性,而非一直想維持策略的一致性。(圖7)

圖7、策略在波動性高的報酬圖

圖7、策略在波動性高的報酬圖

4.從策略在空頭的績效來看,持有現金量過高會是一個問題,進階的策略操作者會設計另一種因應這類情況的策略來補強績效,同時也讓資金使用率上升。這也是從第一個策略建立後,開始向外開枝散葉的起點

5.一週只進出一次會出現無法及時進出的問題,我自己會朝向在SP500跌破200 MA時同時調整部位。不過依照清單的操作週期來看,或許績效提升不是這麼的大。

6.從結果來看,回測結果會很明確地告訴策略制定者,他的策略會在哪些狀況中表現好,哪些狀況中表現不好。而策略修正就會由這些績效圖中切入,進而發展擴充策略,或是補強策略。所以,對於會回測的策略交易者而言,回檔並不是多可怕的事情,Overfitting才是。

7.這類型的替換策略,會出現一個問題。就是選到的標的有可能不會漲,或者是當動能排序落後而被替換的股票開始上漲。這種狀況就是策略必須付出的成本,這部分除非再添加更嚴格的濾網,不然他就是必然發生的狀況,重點在於整體績效是否如預期的上升。

8.使用排序策略會遇到的問題是,當市況下跌時,手中的個股齊跌,部分個股本來獲利,但其他個股跌得太慘,導致這些有獲利的個股動能排序還是非常前面,無法停損。最終觸碰到出場條件時,大多都轉為虧損。這也是策略者需要注意的問題,策略的靈活性很重要,單一策略一定會在某些情況適應不良。

9.對於動能交易者而言,有一點非常重要,通常會挑選高Beta的個股作為投資標的。這類標的會在行情劇烈時表現更大的波動,讓短期投資績效劇烈震盪。這也是動能交易者必須要做的功課,究竟要將策略補強,還是直接透過大濾網,篩出單邊上漲的有利局勢?

10.作者制定的動能策略在2008大幅下跌後的反彈遭遇到問題。由於他卡了一個90天內有出現跳空15%將不被採納,所以在反彈時,出現資金無法買滿的情況。這點很有趣,在極端條件中才有可能遇到。若反彈確立,那也代表他在2009年的表現會大幅落後指數,事實也是如此。(圖8)

圖8、策略在極端狀況可能會出現問題

圖8、策略在極端狀況可能會出現問題

11.作者在書中模擬的動能策略,在實際操作上會遇到一種狀況,也就是下跌破200 MA後,急遽反彈。依照一般動能投資者,應該要抓的到這一整波的利潤,但作者設立了200 MA以上才買股這個限制,導致指數只要低於200 MA,後續到200 MA以上的反彈波段,就會全部都吃不到。這部分也是策略可能要進行強化的部分。(圖9)

圖9、策略遇到大盤大幅下跌後的反彈的報酬圖

圖9、策略遇到大盤大幅下跌後的反彈的報酬圖

策略分析

1.作者首先針對大濾網(200 MA)進行分析,探討是否有設立的必要(圖10)。策略執行的15年內,趨勢濾網發揮兩次功用,確實可以阻止大幅下跌的狀況,這也使得最終績效的差異。作者認為,千萬不要為了某些情況做最佳化改善,他會使結果變的不切實際。針對概念進行交易,需要某種形式的長期趨勢濾網,但如何設定這個濾網,則不太重要。

圖10、趨勢濾網設立與否之績效圖

圖10、趨勢濾網設立與否之績效圖

2.作者使用風險平價指標來設計部位控制是很有趣的做法,一般而言都會以市值大小來設定持股比例,或者是平均部位規模。這樣的作法會導致整個投資報酬被少數高波動的個股牽著走。使用風險平價,目的就是希望每檔個股每天波動都是接近的,並透過再平衡不斷調整,以免讓整個投資組合績效走偏。

3.指標挑選的區間要有建設性,書中以90個交易日作為區間,進行動能計算與排序。他可以反映中期動能,策略的合理性很重要,而非花大把時間追求最佳化。

4.部位規模也具有重要性,當投資組閤中的成分股過少,受到事件交易的風險會太高,提高投資組合的績效波動性。但也不需要過多,會有管理方面的困難,作者經由回測建議,投資組閤中持有約20~30檔即可。

我的看法

對於策略制定,我很推崇作者的想法。在設計策略的開頭,追求的是「合理性」而非「最佳化」。例如你該如何使用一個大濾網區分熊牛、你該如何設立一個參數來排序股票清單,以及進出場的邏輯。方法簡單沒關係,重點是有邏輯。

作者認為所有參數挑個大概就好,不用再進行調整,不用汲汲營營進行最佳化,最後得到的報酬率與MDD或許差異不大。而我看完的想法是,初代策略必定會在某些情況中表現很差,回測的功能是藉由形貌來確認表現優異與表現不足的地方,並且透過策略擴充或是策略補強來改善績效。

在第一次設計策略,幾乎會走彎路,但最重要的是先做出一套骨架,接著再進行雕琢,或許策略不是沒用,而是不適用於所有情況。找到策略最適合的情況,就是策略交易者必須要做的功課。


[賺贏大盤的動能投資法] 讀書心得及實驗

作者Andreas F. Clenow是避險基金經理人, 在這本著作裡, 示範如何回測並建構一支動能交易策略, 並打敗大盤.

書中在建構模型的過程非常強調”資料的正確性“回測大盤最好要考慮現金股利、下市, 和股票分拆.”及持股的分散性, 大概20~30支, 作者覺得差不多, 再多就會和大盤表現貼近, 若太少5~10支則容易受到個股的風險影響(美股沒有漲跌幅限制的….有可能會殺很大..)

S&P500本身就是一個動能策略

蛤? 我看錯了嗎? 作者說, 因為要入S&P500必須市值成長到53億美元以上(也就是漲很久了..), 並且在NASDAQ或NYSE掛牌交易, 也就是要漲一段時間才會被選入, 這點也就是動能策略的主要元素”買進上漲中的股票.

注意”趨勢策略(Trend following)”和”動能策略(Momentum)”還是不同的, 趨勢策略通常會有一條或二條或三條均線去給他當基準, 而均線在盤整時是會失效的, 因此比較適合放在多個相關性低的市場, 互相Cover,

而動能策略就主要是把最會漲的挑出來, 然後加上一個大盤大跌的卡關, 避免大盤走空的時候還持續買股.

作者策略是這樣的:

  1. 用90日漲幅、迴歸斜率及R2找出S&P500中的動能前30名.(也可以是S&P 400, S&P600, 總之要有一個股票池.)
  2. 取前20支持有, 部位大小以ATR決定.
  3. 如果大盤在200日均線以下, 不買股.
  4. 如果個股跌穿100日均線/90日內有跳空15%/從S&P500剔除, 砍!
  5. 每2週(或一個月)再平衡一次.(20支個股要再平衡, 要費一點工夫.)

書中有教怎麼做第1點的Excel表格, 要撈500支S&P500個股的過去90日報價, 再分別計算出斜率和R2, 最好還是要會寫程式比較好.

有趣的是, 作者有比較過只取過去90日漲幅最大的個股, 其實績效和算斜率的個股沒什麼差別, 不過還是嚴謹一點用有斜率和R2的資料, 目的是抓到走勢一致, 沒有暴衝的個股, 作者在平衡波動性這點上很謹慎….若是一般投資人要簡單作就是90日漲幅前20名去買就好了.

個人覺得和Gary Antonaci的Daul momentum滿像的, 就有某種機制挑出強勢股持有, 然後會有一個切換點, 不管是12月報酬還是大盤的200日均線也好, 就是會有一個在大盤不好時跳脫的機制.

總體來說, 阿批還是比較喜歡這種能夠量化的交易手法, 點到出手, 沒有模稜兩可, 也不用去找護城河、經理人品格這種比較難界定的東西,

不過相對而言, 對資料蒐集、資料分析、策略建構的要求就比較高, 程式的能力還是練一下好些, 不然像阿批就有很時候必須靠別人的報告, 或是要牽就網站的格式, 自己想知道的東西就比較不能隨心所欲的去了解….

但量化策略也不是萬能就是了, 回測結果也不見得就一定代表未來, 也是會有過度配適, 或者因為市場改變, 策略失效的問題, 最好還是有多支策略搭在一起用.

不論如何, 任何的投資方式都好, 會長期賺錢就是王道.

冠軍策略的隨機選股測試

書中有一個很帥氣的回測, 作者把S&P500裡的個股,每月隨機挑50支持有, 結果竟可以穩定的打盤大盤**, “要打敗市場如此簡單,** 你確定還要指數報酬嗎**?”**

阿批不禁好奇起來, 如果在臺灣50和臺灣中100中就給他挑過去90天最強的20支來抱, 每月更新, 會不會也能海放大盤? 畢竟買高賣更高、強者恆強, 這樣的動能現象在市場裡不是什麼新聞, 也有研究報告指出動能因子能夠長期打盤大盤.

基於工具的限制, 阿批決定用冠軍策略來作實驗.

股票池

阿批依作者的建議, 把自己的冠軍策略投資組合, 以MTUM為股票池, 從裡面隨機拉出來50支, 原因是Portfolio visualizer的限制是50支.

延伸閱讀: 真的假的? 真的假的? 1個年複利報酬60%的懶人美股及美股ETF投資組合!?

MTUM本身就是動能型ETF, 從美國大型及中型個股中挑出6個月及12個月內表現強勢的股票, 經過波動性調整後給一個動能分數, 再用市值加權, 決定持股比例, 最大不超過5%, 每半年再平衡一次.

然後再使用冠軍策略, 從50支裡面每月拉最強的12支出來(理由是Portfolio visualizer的限制是50支/12支, 程式段位不夠的悲哀..)

歷史持股資料不可得, 不過可以像作者一樣玩, 阿批把MTUM的121支持股篩選後, 留下2014年前上市的79支(原因是有的2020年才上, 就只有一年資料.., 回測結果就會是一年, 用2014年有7年, 不長不短.).

2021/2 MTUM全121支持股如下, 紅色部份是2015以後才上市的股票, 仔細看還不少是近期的飆股, 像Paypal, Zoom, Roku…等等.

img

去掉紅字後剩79支如下, 再從79支中隨機挑50支當股票池, 每月持有最強的12支.

img

測試條件及參數

以下冠軍策略實驗均使用單一絕對動能: MTUM, 出場持有資產: VGLT, 持股數:12

切換參數同原版冠軍策略.

img

實驗一:

挑選的50支股票以藍色標示如下:

img

Portfolio visualizer回測結果:

毫無懸唸的海放大盤, 而且標準差及MDD都很夠水準.

img

實驗二:

挑選的50支股票以藍色標示如下:

img

Portfolio visualizer回測結果:

img

實驗三:

挑選的50支股票以藍色標示如下:

img

Portfolio visualizer回測結果:

img

實驗四:

挑選的50支股票以藍色標示如下:

img

Portfolio visualizer回測結果:

img

實驗五:

挑選的50支股票以藍色標示如下:

img

Portfolio visualizer回測結果:

img

實驗六:

挑選的50支股票以藍色標示如下:

img

Portfolio visualizer回測結果:

img

實驗七:

挑選的50支股票以藍色標示如下:

img

Portfolio visualizer回測結果:

img

實驗八:

挑選的50支股票以藍色標示如下:

img

Portfolio visualizer回測結果:

img

實驗九:

2007年以前上市的共62支, 挑紅字的50支.

img

Portfolio visualizer回測結果:

img

由於想看一下策略在金融海嘯時的表現, 阿批把單一絕對動能換成VFINX(S&P500的美國境內基金), 出場持有資產換成VUSTX(美國長期債券基金).

img

結果, 經歷海嘯不論在報酬率和年報酬都很堅挺, 同時期原版冠軍策略的複合年報酬率是15.94%, MDD是-19.67%, 標準差13.61%.

img

實驗十:

挑選的50支股票以藍色標示如下:

img

Portfolio visualizer回測結果:

img

結論

在股票池裡隨機選股, 真的隨便都可以海放大盤ㄟ….

這樣我想可以一定程度的證明動能策略的是有作用的, 在適當的股票池裡去隨機選股的確能幹掉大盤, 特別作者是有考慮到S&P 500的持股變化, 那麼回測的效度就更高.

但MTUM是每半年調一次持股, 這回持股121支中近3年上市(2019,2020,2021)的個股就有17支, 佔14.05%, 在這樣的狀況下, 2014年的MTUM持有的強勢股到底和現在有多大的不同無從得知, 只能說這個實驗是假設股票池從2014年就是一樣的, 從裡面隨機挑股還是能夠有不錯的績效, 讓阿批確認了相對動能的有效性.

2021/11/14 補注: 請注意本篇在撰寫當時的假設是用MTUM的”現有持股”去做回測, 但MTUM每半年都會換股, 建議到黑石的網站找每月的持股來回測, 結果會大不同.

考量冠軍策略本身自帶的擇強持有、閃避空頭盤的能力, 以及不管MSCI, 亦或本書的作者的對動能策略回測, 亦或阿批自己的實驗及回測經驗, 我想這個模式還是值得一試的, 歐印…沒啦…就倉位不要太大, 風控做好就可以了.

最後警世一下, 如同作者說的, 組合太少會有個股風險, 也確實看到了, 在沒有漲跌幅限制的美股….滿恐怖..買進個股時永遠要記得考慮風險, 適度分散才是.

img

前一篇的組合正犯了欠缺分散性的誤區, 在此也把他修正過來.

股價向上突破是經常遇到的事情,但有的突破能夠持續上揚,屬有效突破;有的突破卻半途而廢,衝到前期阻力位附近時掉頭向下,將投資人套牢在高位,屬無效突破。那麼什麼樣的突破屬有效突破,什麼樣的突破屬無效突破呢?對投資人而言,怎樣才能識別出假突破呢?這裡根據長期的實戰經驗,歸納出真假突破的一些特點。

1. 突破時所處的位置或階段

如果處於底部吸貨區域、中途整理區域、主力成本區域附近的,若向上突破,其真突破的概率較大,若向下突破,其假突破的概率較大。如果處於高位出貨區域、遠離主力成本區域的,若向上突破其假突破的概率較大,若向下突破其真突破的概率較大。

2. 經過整理後的突破才有效

有效突破一般都建立在充分蓄勢整理的基礎上,充分蓄勢整理的形式有兩類:一類是我們常見的各類形態整理,如三角形整理、楔形整理、旗形整理、箱體整理等。

另一類是主力吸完貨以後,以拖延較長時間作為洗盤手段,或者因等待題材或拉昇時機,長期任憑股價回落下跌,股價走出了比形態整理時間更長、範圍更大的整理。股價一旦突破此種整理盤面,則往往是有效突破。由於這種整理超出了形態整理的範圍,因而有時候是難以察覺和辨別的。

3. 大盤的強弱度和板塊聯動

一般而言,當大盤處於調整、反彈或橫向整理的階段時,個股出現放量突破是假突破的可能性較大;而當大盤處於放量上升過程中或盤整後的突破階段時,個股出現放量突破是真突破的可能性較大。而個股突破時板塊聯動同時向上,則可信度較高,這時要選擇量能最大、漲幅最大的個股,這往往就是板塊中的龍頭股。最後還要看政策面和基本面,有無支持該板塊向上的理由。

4. 成交量大小與 K 線形態

在股價創出新高時,如果成交量不能持續放出,這是假突破的最大的特點。為什麼要放量呢?因為股價突破前期多個高點,有大量的套牢盤會放出(前期高點越多,越需要大的成交量),再加上有部分獲利盤發現到達前期成交密集區,會先減倉操作。如果放出大量,並收出小上影線或光頭大陽線,表示主力此次上攻不是試探,將賣盤通吃。這樣的資金實力不是主力又會是誰?幾乎可以肯定地說,這就是主升段的啟動訊號。

一般來說,前期籌碼無明顯發散的個股,特別是一直在集中的個股,在突破前期高點時,無須放出巨量,但應至少要大於前期頂點時的成交量,且在突破點之後還要持續放量一段時間,由此說明突破有效。但如果突破時成交量比前期高點還小,突破後即縮量,則說明突破無效,為假突破的可能性較大,此時應果斷賣出股票,否則就會套在相對高點。

如果前期明顯有籌碼集中跡象的個股,可以在創出低點時少量跟進,與主力共舞。另外,前期有主力的個股,在突破時必須放出巨量,且持續放巨量,證明為有重大題材在後的真突破。

個股突破之前放量上漲,拉出中大陽線,而突破時放量跳空,則可信度較高。此大陽線與跳空缺口稱為突破大陽線、向上突破缺口,極具分析價值。股價突破之後,由於要清洗浮籌,減輕上行壓力,往往要整理或收出長上影線 K 線,但量能要逐步萎縮。

通常成交量是可以衡量市場氣氛的。例如,在市場大幅度上升的同時,成交量也大幅度增加,這表示市場對股價的移動方向有信心。相反地,雖然市場飆升,但成交量不增反減,則表示跟進的人不多,市場對移動的方向有所懷疑。

趨勢線的突破也是同理,當股價突破阻力線後,成交量如果隨之上升或保持平時的水準,這表示突破之後跟進的人很多,市場對股價運動方向有信心,投資人可以跟進,搏取巨利。然而,如果突破阻力線之後,成交量不升反降,那就應當小心,防止突破之後又回復原位。

事實上,有些突破的假訊號可能是由於一些大戶行為所致。但是,市場投資人並沒有很多人跟隨,假的突破不能改變整個趨勢,如果相信這樣的突破,可能會上當。

5. 主力的出貨量與建倉量

假突破由於主力出貨量往往會很大,而真突破量能通常比較溫和,資金性質是明確地向場內介入,雖然有時也會引發放量突破,但只要資金性質沒有改變,便可以跟進。所以說,區別真假突破的重點為,區別主力是不是在進行出貨,只有做出準確的判斷才能迴避假的突破。

假突破的風險性就在於主力藉助巨量進行出貨,因此 K 線形態並不是主要的,主要在於成交量的變化。一般來講,主力的出貨量必然會引發突破的虛假,而只有主力的建倉量才會導致真實的突破。但是,同樣的放量,什麼樣的量是出貨?什麼樣的量是建倉?很多投資人是很難弄清楚的,等到弄清楚了股價要麼已經跌很多了,要麼已經衝上天了。

所以,建議判斷能力不純熟的散戶在分析突破的時候,要儘量避免操作放巨量的股票,除非有能力識別出量能放大的含義。

有很多資金實力雄厚的主力在突破的時候,也不是完全以放量的形式突破,有一些股票的突破都是以縮量或是不放量的狀態完成突破的,這是因為股價雖然創出新高了,但是誰也不肯賣出,這說明持股心態穩定。

由於主力持倉量是巨大的,這表示主力根本不想賣出,主力在當前位置不賣,股價必然還有更高的高點出現,所以對於無量突破的股票,一定要敢於操作。這是因為成交量的萎縮可以限制主力的出貨,當然,股價的波動絕不可能全是縮量突破這麼簡單,放量突破要比縮量突破帶來的收益更大,因為真正的放量突破是資金的建倉區間,主力採用這麼猛烈的手法建倉,股價必然會短線暴漲,所以從獲利的速度來講,放量突破帶來的收益是最高的。

縮量突破可以限制主力的出貨行為,但只有那些高控盤的個股,才可以形成縮量突破走勢。可惜的是,很多個股並不是高控盤股;此外有些高控盤的個股,也需要在突破點因賣盤增多時進行增倉操作。這樣一來就會有大量的股票在突破時,形成放量突破的走勢。

放量突破走勢對於投資人來講是又愛又恨的,愛的是有些放量突破的個股形成突破後會快速的上漲,恨的是有些放量突破的個股卻成了假突破而引發風險。

6. 股價的突破與均線系統

股價向上突破後,一般會沿著 5 日均線繼續上行,回檔時也會在 5 日均線附近止跌,5 日與 10 日均線呈多頭排列。但是假突破就有所不同,股價突破創新高後,就開始縮量橫盤。讓投資人誤以為是突破後的回測確認,但在回檔時股價卻跌破了 5 日均線,繼而又跌破 10 日均線。當 5 日與 10 日均線形成死亡交叉時,假突破就可以得到確認。

股價出現第二次交叉(黏合)向上發散,以真突破居多。股價大幅上漲之後均線出現第3次、第4次向上突破,以假突破居多。這也就是為什麼技術分析專家對均線初次交叉(黏合)向上發散和均線再次交叉(黏合)向上發散格外關注,但對3次4次就不那麼推崇的緣故。因為沒有隻漲不跌的股市,熱點需要轉換,板塊也需要輪動。長線大牛股不是沒有,只是市場不多而已。

7. 突破與突破之後的走勢

股價上漲必須有氣勢,走勢乾脆俐落,不拖泥帶水。突破後並能持續上漲,既然是突破就不應該磨磨蹭蹭,如果放量不漲就有出貨的嫌疑。而且,突破要成功跨越或脫離某一個有意義的位置,比如一個整數點位、一個整理形態、一條趨勢線、一個成交密集區域或某一個時間之窗等,否則判斷意義不大。

8. 股價突破前的時間要求

⑴ 低位突破:股價長期持續下跌,然後在低位橫盤,只要在低位時間足夠(超過 3 個月以上),股價在低位兩次向上突破時以真突破居多。反之,當時間小於2個月時,向上突破往往以假突破居多,這也是形態理論的要求。

⑵ 高位突破:個股高位橫盤整理,整理時間越長,向上突破越有效。

9. 股價突破後的側向運動

在研究趨勢線突破時,應當明白一種趨勢的突破後,未必是一個相反方向的新趨勢的立即出現,有時候由於上升或下降太急,市場需要稍作調整,出現上下側向運動。如果上下的幅度很窄,就形成牛皮狀態。側向運動會持續一些時間,幾天或幾週才結束。

側向運動會形成一些複雜的圖形,結束後的方向是一個比較複雜的問題。有時候,投資人對於股價來回窄幅運動,大有迷失方向的感覺。其實,就意味著上升過程有較大的壓力,下跌過程有買盤的支撐,買家和賣家互不相讓,你買上去,他賣下來。

在一個突破阻力線上升的過程中,側向運動是一個打底的過程,其側向度越大,甩掉牛皮狀態上升的力量也越大。而且,上升中的牛皮狀態是一個密集區。同理,在上升過程結束後,股價向下滑落,此時所形成的密集區,往往是今後股價反彈上升的阻力區,就是說沒有足夠的力量,市場難以突破密集區或改變下跌的方向。

10. 發現突破後應多觀察一天

如果突破後連續兩天股價繼續向突破後的方向發展,這樣的突破就是有效的突破,是穩妥的買賣時機。當然兩天後才買賣,股價已經有較大的變化:該買的股價高了、該賣的股價低了。但是,即便如此,由於方向明確,大勢已定,投資人仍會大有作為,比之貿然操作要好得多。

同時,注意突破後兩天的高、低價。如果某一天的收盤價突破下降趨勢線(阻力線)向上發展,而第2天的交易價能跨越其最高價,表示突破阻力線後有大量的買盤跟進。反之,股價在突破上升趨勢線(支撐線)向下運動時,若第2天的交易價是在它的最低價下面運行,那麼表示突破趨勢線後,賣盤壓力很大,應及時做空。

作者簡介_麻道明

又名邵道明,著名證券技術分析師、財經評論員,被散戶稱為「主力剋星」。2015年獲得「金操盤」炒股實盤大賽第一名,2015年「中國投資者交流賽」前八強。曾於私人機構操盤,親歷主力歷程,掌握主力內幕。在沒有硝煙的股市戰場上,經過20 多年的千錘百鍊,摸索出獨特的操盤手法,成為股市操盤高手。

本文摘自大樂文化出版 《最狂「主力剋星」教你用 140張圖學會 技術線型賺大波段

投資哲學

操作

  ■ 資金控管與試單策略

  ■ EMA1030交叉法

  ■ 如何出場

  ■ 關鍵點操盤術

選股

  ■ 黃金律:買進強勢類股中的最強勢股票

  ■ 產業大趨勢選股

  ■ 技術面選股

  ■ 低價轉機股

  ■

原理

  ■ 價格走勢才是終極真理

  ■ 為何需要多元系統

技術分析

  ■ 技術指標MA

  ■ 帶柄杯狀排列

  ■ 3C與其他有用型態

資金控管與試單策略

https://htm0606.pixnet.net/blog/post/406263802-%E8%B3%87%E9%87%91%E6%8E%A7%E7%AE%A1%E6%B3%95?fbclid=IwAR2k9DWoI8dGox8EDvM1j4bnLkRnT_82zlaMwlGVg2imrOoaOsUix8si11w

  1.討論你要投入多少錢(即部位)進入股市?專家建議你要「由小而大」一開始先小部位,有賺錢才逐漸加大部位。

  2.每一張股票要投入多少錢?專家建議每一筆交易的金額都要一致,這樣才不會被運氣左右績效。

  3.每一張股票是一次投入所有的資金還是分批買入?李佛摩建議使用試單策略。

你要投入多少錢?

  Larry Williams說道:「投機客生財之道來自他們的資金管理方法,而不是一些神奇的、神祕的系統或煉金術士的祕方。成功的交易會賺錢,成功的交易加上適當的資金管理則會創造龐大的財富。」(「短線交易祕訣」)

  在「交易‧創造自己的聖盃」/ 凡‧沙普一書中,也把「部位大小」的設計作為交易上最關鍵的技術

  Victor Sperandeo在1978年到1989年的12年間,沒有一年的操作發生虧損,平均年投資報酬率高達70.7%,而被Barron's譽為「華爾街的終結者」,由以下所述的投資哲學,可以看出Victor對於「資金控管」尤為重視。

  「...假定你是以季為基礎操作。在一季的開始,任何新部位的規模都應該很小(相對於風險資本而言),因為當期還沒有累積獲利。...反之,如果你有獲利,應該將一部份獲利運用在新部位上,並將其餘獲利存入銀行;如此,你不但可以增加獲利的潛能,又可以保障一部份的獲利。...」(詳見後文)

  本文討論資金控管的方法,以下各種方法本人較喜歡Mark Minervini的「由小而大」。

  馬克的方法簡述如下:由小而大,把資金分為三個部位大小(25%,50%,100%),最大虧損為每個部位的25%,規則一:總獲利賺到某個部位的最大虧損,部位就提升為那個部位,規則二:總獲利小於某個部位的最大虧損,就降級到總獲利仍大於該部位最大虧損之部位。

※超級績效的舉例很難懂,倒不如用歸納法歸納為2個規則。

  以下以總資金$4000說明如下:

部位大小A(25%)B(50%)C(100%)
$1000$2000$4000
最大虧損25%ALBLCL
$250$500$1000

  第一次交易,起始部位為A($1000),如果賺到$500>=BL(規則一),則部位提升為B($2000)。

  第二次交易,如果再賺到$500,則總獲利為$1000>=CL(規則一),則部位提升為C($4000)。

  第三次交易,若虧損$500,則總獲利為$500(1000-500)<=CL AND >=BL(規則二),則部位降為

以下列出各家的資金控管方法供參考,你可以選擇你喜歡的。

※上表可下載google試算表範本【資金控管計算範本

每一張股票要投入多少錢?

  專家建議每一筆交易的金額都要一致,這樣才不會被運氣左右績效。

  比如說你買2支股票,一支賺10%,一支賠10%,你的績效本應是0,但因為股價不同,如前一支股價100元賺10%,是賺10元,後一支股價20元賠10%,是賠2元,總計賺8元,但其實是因為運氣造成績效的波動。每筆交易金額不一致,即使你使用一套正報酬率是20%的無敵系統,得到的績效也是被運氣主宰的。

所以你必須每一支股票交易金額都一樣(資金不大者可用零股),才能維持穩定的績效表現。在【資金控管計算範本】中有「零股計算」工作表,如下:

A股票檔數10.00
B可投入資金(千元)250.00見[資金控管法之部位]
C每檔股票可投入金額(元)25,000.00=B*1000/A
D股價100.00
E可買零股股數250.00=C/D(四捨五入)
F半刀125=E/2
G投入金額12,500

  說明如下:

  A股票檔數:基本面操作者建議不要超過7檔,技術面操作者可10檔。

  B可投入資金(千元):就是前面的你要投入多少錢?比如你股票總資金為100萬,一開始只投入25%就是25萬,則B=250(千元)

  C每檔股票可投入金額(元):=B*1000/A,

  D股價:輸入股價

  E可買零股股數:=C/D

  F半刀或三刀或四刀:

  李佛摩提到:「很多投機客在買進或賣出時都太衝動,幾乎所有部位都是在同一價位取得,而那是錯誤且危險的作法。」李佛摩採用的試單操作方法,是將資金分成20%、20%、20%、40%,試單時先投入第一筆20%的資金,如果行情發展正確(帳面上呈現獲利),才會再投入後續的資金部位。

我的試單策略

  使用三刀流。自上次買進點漲3%加碼一次,共加碼2次。

  雖然在測試中(見C:\Users\eagle9971\Google 雲端硬碟\我的投資\StockSD\系統測試\BBBRO_1.ods\試單策略Y))6刀流在行情好的時候可以獲得「讓我一次賺個夠」的效果,但也只有在2020/6/18~2022/4/22這種史無前例的瘋牛行情中出現,而三刀流在12年的測試期間表現得比四刀流「穩定」,犧牲一點獲利(<3%),所以決定使用三刀流。

  而變動出場點為-12%,-6%,0%。指總獲利1批時-12%,2批時-6%,3批時沒獲利就出場

一日二刀流

  所謂的「自上次買進點漲3%加碼一次」是指隔日尾盤嗎?本來無此限制,比如原先跌5%時買進第一筆,盤中漲3%加碼1次,再漲3%再加碼1次,三刀流一日內就能完成全部部位。但要考慮實際情況,如果盤中所有部位買滿,然後從漲停跌到跌停,你有沒有辦法及時出脫?除非你有融資、融券可當日沖銷,否則你只好等到明日開盤再賣,如果明日開盤再跌停,結果你一次就是滿檔虧損20%。這種情形會不會發生?老實說,不常見但有機會發生。

  所以,最多你「一日二刀流」,第二刀必須在尾盤才出手。這樣就不可能加碼後變成跌停(因為已經是尾盤)。那如果一日尾盤漲幅超過6%是否一次加2碼?還是不要,因為大漲後回檔的機率很大,沒有必要讓自己陷於險境。

參考文章

由小而大

(摘自「超級績效2--投資冠軍的操盤思維」/ Mark Minervini)

  我渴望達成偉大而高貴的工作,但我最重要的任務,就是把小工作都當成偉大而高貴的工作來做。 ——海倫,凱勒

人生的任何重大成就,都是由小而大,慢慢累積而成。 股票交易也是如此。 這不是一種「不成功便成仁」的活動,沒有必要將其視為全有或全無的決策,你大可一點一滴慢慢來。 我幾乎從未奮不顧身地跳進股票市場,反之一般會先試試水溫,建立幾個規模很小的部位。 如果情況順利,我才會加碼,或增添其他部位。如果情況仍然符合預期,我才會增加整體曝險,態度變得更為積極。如此才不至於招致麻煩,當一切都順利時, 才具有賺大錢的條件。

  剛開始從事股票交易,應該先從現金部位開始,而且要等到立足點穩定之後,交易規模與整體曝險程度才應該慢慢增加。 關於擴大交易規模,我秉持簡單的哲學:如果二五%或五O%的現金部位都無法賺錢,那為什麼要擴大曝險程度到七五%或百分之百,甚至動用融資呢? 事實的情況剛好相反:交易發展如果不符合預期,應該考慮縮小交易規模,或是繼續維持現狀。

  嚴格遵循交易規則,每逢交易不順利,就縮小部位規模;因此,當交易最不順遂的時候,你的交易規模呈現最小狀態。這才符合風險控制的原理!反之,如果抱著「輸多賭大」的心理,交易愈不順利,愈是增添曝險程度,則在交易最 不順手的時候,你將持有最大部位,這等於是在替自己找麻煩。

  以上這方面的紀律,功能不全然在於防禦。 嚴格遵循這項法則,部位曝險將在交易最順手的時候,擴張到最大程度,這才有助於創造超級績效。 所以,你是讓資金發揮複利作用,不是虧損,但前提是你必須嚴格遵循這項法則。

  總之,部位如果呈現虧損,實在沒有理由擴大交易規模。反之,交易如果順手,可以運用一帳面獲利融通部位,而且不至於增添風險。

  容我稍微說明我的作法:我通常最初只建立四分之一的部位(請參考圖表5-5)。賺錢之後,我會讓既有部位擴大一倍,直到持有完全部位為止。我的賺錢部位會擴張規模,虧損部位則縮減規模 。

  假定我的盈/虧比率為二:一,勝率為五O%。 如果交易順利而得以連續進行三筆成功交易四分之一部位,二分之一部位,以及全額部位其獲利將足以讓我融通三個完整部位和一個二分之一部位(請參考圖表5- 6)。

img

img

圖5-6說明

部位大小A(25%)B(50%)C(100%)
$1000$2000$4000
最大虧損25%ALBLCL
$250$500$1000
交易序次投入部位獲利總盈虧備註
1$1000$500$500獲利達BL,擴大部位至B(50%)
2$2000$1000$1500獲利達CL,擴大部位至C(100%)
3$4000$2000$3500獲利達CL,繼續維持部位C(100%)
4$4000-$1000$2500獲利仍達CL,繼續維持部位C(100%)
5$4000-$1000$1500獲利仍達CL,繼續維持部位C(100%)
6$4000-$1000$500獲利只達BL,縮小部位至B(50%)
7$2000

幽靈規則二

  「毫無例外並且正確的對你的獲利部位加碼。正確的交易本身實際上並不能產生豐厚的利潤。如果你真的希望自己擁有以交易謀生或賺外快的能力,你就應該在你的獲利倉位上增加籌碼。否則的話,你最多隻能保本。」

幽靈理論的見證

出處:https://www.cmoney.tw/notes/note-detail.aspx?nid=13886

掌握加碼2大原則!讓你避免風險,獲利加倍!

7月 2014年

  到底要怎麼加碼才對?1.有獲利了,才能加碼下單2.下跌時千萬不能追買攤平成本,這樣只會越攤越平這邊用2個故事來跟大家分享,贏家們他們是怎麼做加碼的。

期貨當沖高手的加碼策略

  在我的公司裡面,有一位每天來看盤下單的大哥,在期貨市場10多年,已經賺到了好幾棟房子,但每天的穿著卻是很簡樸,在這邊早上安安靜靜的在下單,下完收盤後就走了。 我很好奇他怎麼有辦法做到這樣,10多年來都靠期貨為生,

  我就厚著臉皮開口問他,他人很好,跟我講了一個很重要的觀念。

  「我賠錢時永遠只賠 1 口,賺錢時卻是 5口、20口在賺!」這位大哥說。我問,什麼意思?

  他說 「我還沒有很明確趨勢出來以前,我會先用一口單試單,嚴守停損。」

  「一旦趨勢很明確出現了,我有獲利就代表我做對邊,我就會開始加碼。」

  「所以,一整個波段抱下來,我都是10口、20口在賺的。」

程式交易高手S先生的加碼策略

  又有一個很好的機會,跟一位程式交易的贏家Steven聊天(有出書的作者), 發現他的加碼邏輯很值得學習。他說,他永遠都是把他的風險值賺到了,才允許進場訊號下單。什麼意思?

  假設,我總資金100萬。 我每一筆交易的風險是1萬元。也就是每一筆交易,要是虧損超過1萬元,就立刻砍倉。所以每一次的交易風險只有 1%,不會傷到本。

  舉例:假設我在8500買進一口期貨契約(1點200元),風控50點(1萬元)。假如行情向上,上漲到了8550,這時我的未平倉獲利已經到了1萬元,是不是跟我的風險值相當了?那這時,就允許加碼第二口單。

  因為第二口單下單時,你口袋已經有1萬的獲利了,所以第二口單,幾乎等同於新單。

  這時,停損點跟著上移到8500。然後這時候,若是出現反向出場訊號,全出。

  (上面這個例子只是概念說明,並不是他的程式真的這樣執行喔~)

  為什麼要這樣做?因為我們無法預測明天,我們只能在目前的市況,站在機率比較高的那邊交易,但這也是無法100%保證一定會照我們想的走。所以,我們在試單階段,要一定要先用很小很小的部位試單。小心謹慎操作。...

資金管理才是輸贏的關鍵

  在賭場理最常見到賭客賠掉所有賭金的方法是:輸的時候加碼再玩,也就是說,越輸就玩越大。贏家會反其道而行,輸的時候減碼經營,贏的時候大膽出擊。當然,如果每次都全押,他會以可怕驚人的速度一直贏到最後一次,那一次把之前贏的全部歸零。你的資金必須有適當的管理,每次全押,或越賠玩越大的方法都是必須避免的。

  Larry Williams說道:「投機客生財之道來自他們的資金管理方法,而不是一些神奇的、神祕的系統或煉金術士的祕方。成功的交易會賺錢,成功的交易加上適當的資金管理則會創造龐大的財富。」(「短線交易祕訣」)

  在「交易‧創造自己的聖盃」/ 凡‧沙普一書中,也把「部位大小」的設計作為交易上最關鍵的技術。

  Victor Sperandeo在1978年到1989年的12年間,沒有一年的操作發生虧損,平均年投資報酬率高達70.7%,而被Barron's譽為「華爾街的終結者」,由以下所述的投資哲學,可以看出Victor對於「資金控管」尤為重視。

Victor:一致性的獲利能力

專業投機原理 - Victor Sperandeo / ISBN:9578457189寰宇

  ...**如果資本要穩定增加,你必須要有一致性的獲利能力;如果你要有一致性的獲利能力,必須要保障你的獲利,並儘可能降低損失。**因此,你必須衡量每一項決策的風險與報酬的關係,根據已經累積的獲利或虧損評估風險,如此才能增加一致性的勝算。

  例如,假定你是以季為基礎操作。在一季的開始,任何新部位的規模都應該很小(相對於風險資本而言),因為當期還沒有累積獲利。另外,你應該預先設定承認自己錯誤的出場點,一旦行情觸及這個價位,你便應該認賠出場。如果第一筆交易發生虧損,任何新部位都應該根據損失而按比例縮小。依此方式交易,任何一季結束時,你都不會虧損所有的風險資本--你永遠還有籌碼。反之,如果你有獲利,應該將一部份獲利運用在新部位上,並將其餘獲利存入銀行;如此,你不但可以增加獲利的潛能,又可以保障一部份的獲利。

  如果我是一位年輕的投機者,並擁有5萬美元的資金交易商品期貨,我最初的部位不曾超過總資本的10%--5.000美元並設定停損而將潛在的損失侷限在10%至20%之間--500美元至1,000美元之間的損失。換言之,根據這項設計,我的虧損絕對不曾超過總風險資本的1%至2%。如果第一筆交易發生1,000美元的損失,則次筆交易的部位將減至4,000美元,並將潛在損失設定在400美元至800美元之間。依此類推。

就另一方面來說,如果我第一筆交易獲利2,000美元,我將存入銀行1,000美元,並將次一個交易部位增至6,000美元,這將增加我的起始(initial)風險資本(5.000美元)達20%,而實際(actual)風險資本也增加相同的金額。依此方式,即使我下一筆交易發生虧損,就整個期間來說還是有獲利(譯按:這是指一筆交易最多虧損20%而言)。如果我對於行情的判斷有50%的正確機會,則這種交易策略將可以創造相當可觀的收獲。假設我頂多隻接受1:3的風險/報酬比率,即使我每二筆交易僅有一次獲利,我的收入仍然相當可觀。換言之,如果你每一筆交易,可能的(probable)報酬至少是客觀可衡量(objectively measurable)之潛在損失的三倍,長期下來,你便可以維持一致性的獲利能力。...

三一理論

「孫子兵法與期貨股票實戰」 - 陳富村 / ISBN:9861275002

  依筆者在擲骰子的經驗中,發現資金控管的問題,完全是由市場決定的;贏時加碼,贏三倍以上才可以加碼一倍。例如以美金10元為基本下注,贏30元以上才可加碼為每次20元下注;最好是贏50元才加碼。當加碼後輸時,下次下注立刻減碼為10元,如果又輸,還是10元,再輸還是10元;一贏之後立刻恢復20元,又贏還是20元;等到贏三倍60元時,再考慮加碼為40元;輸時立刻又回到20元,最後例如贏到180元,這時可加碼到60元下注了!但若一輸則立刻回到30元,再輸還是30元下注;當基本金額加到100元與300元層級時,還是要保持三比一的比例;輸絕不加碼,贏了就加碼。以全部的資金為單位,每次最大下注的金額不超過全部資金的百分之十五。因為臺灣的期貨經紀商在下注虧損百分之七十時,就會自動把客戶三振出局;因此用全部資金的百分之十五為最高限制,也就是虧損金額最高不可超過百分之十。這樣的資金控管,進可攻退可守,以保命為最高設計;絕不是由自己的意志來決定下注多少,這是筆者的發明,是由隨機理論的賭骰子中得到的證明。

  用之於期貨交易,虧損不超過一倍。贏時緊咬不放,贏多少由獲利停損決定出場,而不是投資者用波浪理論,或什麼壓力、支撐決定;出場是沒有自我的意志的。因此,資金控管絕不是投資人高興如何下注就怎麼下,投資人所能決定的只有基本額度的下注;加減碼沒有個人意志,這是贏家贏大輸小之法。其重要性比任何技術理論或電腦系統更關鍵,也不需仰賴一大堆公式;那些公式一般人看不懂也不必懂,投資人只要會加減乘除即可!股票加減碼也能比照這原理,投資者可自行調整。

Elder:如何防範鯊魚與食人魚的攻擊

  「... 食人魚是一種熱帶淡水魚,比一個男人的手大不了多少,卻有兩排銳利牙齒。這種魚看起來不是很危險,但如果狗、人或驢子掉進熱帶河流,一群食人魚就會一撲而上,不停撕咬,受害者根本招架不住。一頭走進水裡的公牛若是遭到食人魚攻擊,只消幾分鐘,水裡就只剩下骨骸上下浮沉。交易人以2%準則防範鯊魚攻擊,但仍然需要防範食人魚。6%準則可以讓你不致被一小口一小口咬死。...」-- Elder

Elder的資金管理方程式

以下摘自「走進我的交易室」/ Dr. Alexander Elder

  先計算利潤的交易新手是本末倒置,他們應該先計算風險。如果你遵循2%和6%準則,要問自己:最大的容許風險是多少?

  適當的資金管理步驟如下:

  1. 每個月第一天衡量你的帳戶價值--現金、相當於現金的資產,以及末軋平部位的總和。

  2. 計算交易資金的2%,這是任何一筆交易能夠承受風險的上限。

  3. 計算資金的6%,這是任何一個月容許發生虧損的上限,超過這個上限,你就必須軋平所有交易,停止交易,直到月底。

  4. 每一筆交易都要決定進場點和到價出場點,以金額表示每股或每口合約所能承受的風險。

  5. 拿「交易資金的2%」除以「每股風險」,算出你可以交易多少股或多少口合約。取整數時,是向下取較低的數字。

  6. 計算所有未軋平部位的風險,方法是以「進場點到目前到價出場點的距離」,乘以「股數或合約數」。如果總風險佔帳戶的4%或以下,或許可以再建立另一個部位,因為目前的交易將增加2%風險,使得總風險達到6%。請記住:你不必每股交易都承受2%風險,如果需要,你可以承受較低風險。   6. 只有在達到以上所有條件之後才交易。

範例

  把2%準則加上6%準則,當作你自己的交易經理。我舉個例子來說明,如何利用這些準則進場交易。為了簡單起見,在此假設任何一筆交易拿去冒險的錢佔淨值的2%,但事實上我們希望承受更低的風險。

  一位交易人在月底時計算他的淨值,發現有l0萬美元,沒有末軋平部位。他記下下個月最高的風險水準:每筆交易2%或2,000美元,以及整個帳戶6%或6,000美元。

  幾天後,這位交易人發現股票A非常吸引人。他算好在哪裡下到價出場單,建立了一個部位,承受2,000美元的風險,佔資金的2%。幾天後,他又發現股票B不錯,做了一筆類似的交易,再拿2,000美元去冒險。

  那個星期結束前,他再買進股票C,一樣承受2,000美元的風險。

  下個星期,他看到股票D,比前面三檔股票還吸引人。應該買嗎?不可以,因為他的帳戶已經承受6%的風險。他有三個末軋平交易,各冒2%的風險,如果市場走勢不利,可能虧損6%。6%準則禁止他再拿任何錢去冒險。

  幾天後,股票A大漲,他將到價出場點移到損益兩平點之上。僅僅幾天之前,他不能買的股票D看起來仍然很吸引人。現在他可以買嗎?可以,因為他目前承受的風險只佔帳戶4%。股票B承受2%風險,股票C承受另外2%風險,但股票A沒有承受任何風險,因為它的到價出場點高於損益兩平水準。這位交易人買了股票D,砸下另外2,000美元或帳戶的2%去冒險。

  那個星期稍後,這位交易人看到後市極為看漲的股票E。他可以買嗎?根據6%準則,不能買,因為他的帳戶已經有股票B、C、D承受了6%的風險(他的股票A已經沒有承受任何資金上的風險)。他必須放棄股票E。

  幾天後,股票B下跌,跌到它的到價出場點。股票E看起來仍然很好。他可以買嗎?不可以,因為他已經在股票B賠了2%,而股票C和D仍然承受4%的風險。這時再建立另一個部位,會使他超過每個月承受6%風險的上限。

  6%準則保護你不受食人魚攻擊。當牠們開始撕咬你,就立刻離開水中,不要讓那駭人的魚咬死你。如果你每筆交易承受的風險低於2%,那麼你可能同時擁有三個以上的部位。如果每筆交易只承受帳戶淨值1%的風險,那麼你可以建立六個部位,才會到達6%的上限。6%準則根據上個月月底的帳戶餘額,保護你的淨值,不考慮你這個月可能賺到的額外利潤。

  如果進入新月分時,你擁有很大的末軋平利潤,那麼你必須重新計算自己的到價出場點和交易規模,不讓新的總淨值中,有任何一筆交易暴露在2%以上的風險,而所有的末軋平交易加起來則不超過6%。每當你表現不錯,月底之前帳戶價值上升,那麼6%準則允許你下個月加大交易規模。如果你表現不好,帳戶金額縮水,它就會限縮你下個月的交易規模。

如何出場

https://htm0606.pixnet.net/blog/post/405322031-%e5%a6%82%e4%bd%95%e5%87%ba%e5%a0%b4

俗話說得好,會買股票是徒弟,會賣股票才是師傅。飆股人人都買過,但真正賺到大錢的百不得一。

多重出場點:單純性與多個出場點

  交易系統設計應該採用單純的概念。我們之所以強調單純性,因為這代表相關系統是建構在「瞭解」的基礎上,而不是最佳化。

  單純的概念可以引用到許多不同市場與不同交易工具。我們雖然強調單純,但交易系統仍然可以設定多個出場點。這是兩個不相互衝突的概念,單純性是交易系統之能夠有效的必要條件,多重出場點則是滿足交易目標的必要條件。出場點雖然有很多個,但每個出場點都可以源自簡單的概念。

  讓我們看個例子。假定我們想使用順勢系統,而且希望留在市場久一點。我們不相信神奇的進場訊號,所以要留給部位較大的迴旋空間。另外,萬一出現重大不利走勢,系統必須保障資本,部位必須認賠。最後,由於起始停損相當寬鬆,我們將儘可能獲取較大的利潤,當獲利達到4R時,停止點將設定得更緊密一些。因此,我們要根據這些信念,設計一套適用的交易系統。這個例子顯示一項重要觀念:交易系統設計上必須符合個人信念。這也是交易系統設計的祕訣之一。

  首先,進場點的起始停損必須相當寬鬆,提供充分的迴旋空間,不至於造成訊號反覆而增添交易成本。我們決定採用前文提到的辦法:3倍的價格波動。這是最糟狀況的停損,但也是後續的追蹤型停止點,因為每天收盤價如果朝有利方向變動,我們將依此重新設定停止點。

  其次,我們相信,如果市場出現強勁的反向走勢,就應該結束部位。所以,我們決定,只要任何一天的價格反向走勢超過每天價格波動的2倍(由前一天收盤價起算),就結束部位。這個停止點與前一段的停止點是並存的。

  最後,獲利一旦到達4R,將採用緊密的停止點,避免吐回太多帳面獲利。所以,獲利到達4R之後,停止點將設定為平均真實區間的1.6倍(不是原來的3倍):從此之後,這也是唯一的停止點。

  請注意,這些停止設定都很單純,清楚反映我們所想要的目的。沒有經過歷史測試,所以沒有最佳化的問題。完全沒有涉及火箭科學,所以很簡單。總共有3種停止點,但任何時刻都只有一個停止點真正有效,也就是最接近當時市場價格者。

(摘自「交易‧創造自己的聖盃」/ 凡‧沙普)

「簡單多重出場策略」摘要

  根據上述,它提出的是三個出場點的設計。簡述要點如下:

  1. 初始停損點:進場點的起始停損必須相當寬鬆,提供充分的迴旋空間。

  2. 特殊出場點:如果市場出現強勁的反向走勢,就應該結束部位。

  3. 停利點:獲利一旦到達4R,則採用緊密停止點。

我的多重出場點

  1. 初始停損點:小於10%。

  2. 特殊出場點:如果跌破重要支撐。

  3. 追蹤型停止點:從高點(收盤)回落15%~30%則出場。

  4. 緊密停利點:獲利一旦到達30%~100%,則採用緊密停止點--跌破ma5或ma20(不下移)*0.96。

  5. 彈性原則:浩劫餘生與太空漫步,採取的策略當然有所不同。所以設計有一般波段模式與股災模式。

波段模式

  1. 初始停損點為最大虧損 10%,越小越好但不能太小。

  2. 由最高點回落 20%

  3. 不要求在期限內要漲多少。

  4. 漲幅達到 30% ,改用 max(ma20)*0.96 為浮動停利點。

股災模式

  1. 初始停損點為最大虧損 10%

  2. 由最高點回落 30%

  3. 不要求在期限內要漲多少。

  4. 漲幅達到 100% ,改用 max(ma20)*0.96 為浮動停利點。  

虧損不超過10%

  「...限制虧損之後,雖然有些轉敗為勝的成功交易消失了,但這方面的不利影響,遠低於迅速認賠的有利影響。根據這項虛擬測試,整個投資組合的績效改善程度,實在顯著到到令人難以置信。我重複驗證相關的計算程序,數據都是正確的。我的投資組合表現,從原來的兩位數字虧損,變成了獲利超過百分之七十。」(摘自「超級績效--投資冠軍的操盤思維」/ Mark Minervini)

特殊出場點

  一般情形下原則上虧損不超過10%,但你可以設定的更緊密一點,然而你永遠無法逃過主力裝死、蹲伏、甩轎、洗盤的磨難,所以必須有所瞭解與對策。

交易儘量接近危險點

  「...真正優秀的交易者,他知道如何判斷正常的價格拉回整理,以及具有危險的價格行為,他們會把停損設定在正常回檔即將演變為危險走勢的關鍵位置,然後讓交易進場價位儘量接近危險點。...」(摘自「超級績效2」)

蹲伏與反轉復甦

  「...交易者建立部位之後,皆希望看到股票呈現應有的「行為」,但我們也想避免在不必要的情況下扼殺交易機會。行情沒有立即發動,並不代表相關交易就是失敗。...進場建立部位之後,不妨讓股票保有一、兩個禮拜的時間可以正常波動當然必須維持在停損範圍內。股票如果出線蹲伏情況,不必覺得恐慌,,只要停損沒有被引發,沒有出現主要違例現象,不妨等待看看股票是否會發生反轉復甦走勢。」(摘自「超級績效2」)

重新進場

  「某些股票可能呈現理想的架構,吸引買盤進場,但走勢很快進行修正或急遽拉回,引發部位的起始停損。這種情況之所以發生,通常是因為大盤走勢轉弱或劇烈波動。一般來說,股票的基本面條件如果優異,價格向下修正或拉回之後,通常會再出現新的買點。這種新的買進架構往往較先前的買進架構更為優異,因為籌碼得到更一進一步的清洗。

  部位的起始停損遭到引發之後,不能預期該股票絕對會再出現買進架構;換言之,起始停損一旦遭到引發,就必須停損出場。反之,部位遭到停損之後,並不能排除相關股票再度成為買進對象的可能性;只要該股票滿足了所有買進條件,就可以重新進場建立部位,雖然時機可能相對落後。請注意,真正的重大獲利機會,經常是在重新進場兩、三次才得以掌握。可是,這點往往也是區別真正專業玩家的分野。業餘玩家可能被停損一、兩次之後,就不會考慮再進場,但專業玩家則永遠保持客觀立場,他們只考慮進場條件是否滿足,把每個潛在機會都視為全新投資進行評估。」(摘自「超級績效2」)

  頂尖交易員喬治.席格(George Segal)說:「我相信你想知道,我是如何得知應該何時進場的?在我真正建立部位前可能已出場二、三次(停損出場);有時我可以一次便成功地建立部位。但通常需要嘗試數次後,我才會覺得對盤。我願意隨情況的需要不斷試盤,這是一種獨特的觀念;多數人在市場中挫敗,因為他們只做一兩次的嘗試,往往在節骨眼上放棄。而我是不斷地回頭嘗試,不斷地敲著大門,直到大口敞開為止。」

為什麼不用其他出場法?

  以臺積電為例,本文比較型態學出場法、均線出場法、基本面出場法、潛力股出場法。

使用形態學出場

以臺積電為例。[0066_臺積電漲倍圖]

image

下圖[0067_臺積電型態出場]顯示在2015年,使用型態學出場會被洗出兩次。

image

但這兩次的振盪在整個上漲中微不足道,可見得型態學並不適合長線操作。

使用均線出場

如果使用長線150日線作為出場條件,下圖顯示至少會被洗出三次。

[0068_臺積電均線出場]

image

均線眾所週知的缺點就是「太慢」,如下圖,等到出場已經吐回58%以上獲利。

[0069_國巨均線出場太慢]

image

由從可見,使用均線出場,不是太快就是太慢。

使用基本面出場

以下使用「超級績效--金融怪傑交易之道」/Mark Minervini舉的例子說明,散戶如果要依據公司的營運狀況變差來出場,那叫作「天方夜談」。

[0070_基本面出場太慢1]

image

[0071_基本面出場太慢2]

image

[0072_基本面出場太慢3]

image

[0073_基本面出場太慢4]

image

使用潛力股投資法股災模式

下圖顯示使用股災模式出場法有不錯效果。

[0074_股災模式1]

image

[0075_股災模式2]

image

注意,只有在低檔,你認為有一倍以上的潛力時才使用「股災模式」,如果已經漲到第三波、第四波,就只能使用「波段模式」(預期有30%以上獲利),短線模式不要使用,你應該使用經過驗證過的「短線交易系統」,陰陽線、指標等不是經過驗證的系統,只不過是「見證」,見證與驗證不同,驗證是可複製的科學邏輯,見證大多隻是偶然。

[0076_股災模式3]

image

其他多重出場法

歐尼爾的出場方法

  綜觀大多數名家的出場策略,大都包括兩種以上的不同策略。以威廉‧歐尼爾為例,其出場方法其實甚為複雜,如下:

  1. 初始停損點:8%。2. 停利點:20%。好像很單純,其實不然,因為還有以下幾點;

  3. 走勢超強的個股,即在8週內上漲20%的股票,則不理會20%獲利了結的規則,至少要繼續持有8週。

  4. 8個繼續持有的法則

  5. 36個提早賣出的法則。

達華斯的出場方法

  這個也許不能謂之為單純了,也許找另一個例子比較好,尼可拉‧達華斯:

  1. 初始停損點:箱型頂部往下一盤(買進點為往上一盤),所以其風險報酬率是所有系統中最低的。

  2. 獲利出場點:當另一個箱型建構完成後,停利點往上移到這個箱形的底部下一檔。

  真的是再明確不過了,撇開那個停利點似乎有一些問題(不是每個下跌的股票都會先建立箱型),其實這是一個最佳的範例:多重出場點。

[心得] 2023年報 膽小鬼買法

https://moptt.tw/p/Stock.M.1703859683.A.147?fbclid=IwAR0paSAbFN5L9ZpUfFXKA9ro9-By-jXokqE6fld15JP7rEaRbbT29ocfR7g

想當股神的 可以先左轉了 這方法沒辦法讓你翻倍賺 已實現

https://www.youtube.com/watch?v=KTkkF6DazIA

未實現

https://www.youtube.com/watch?v=PJ5Rga5GKyk

股利沒有去算過 領太多隻了 應該有10萬多吧 大概~ 目前持股43隻 問就是歐印!!! 天天都是滿倉!!!

目前買法 只以基本面去買 不看線型 籌碼 型態 只看位階 營收 獲利 去做選擇 選股 主要是 看毛營利變化 營收 財報細項 法說會 產業變化 公司新聞 去做挑選 然後開始去預估下一季EPS 想知道更細的 可以去看我以前標的文 選法就是所謂的連連看 你確定的越多 對於EPS估算就越有幫助 以前有打標的文 可以去看看

再來是操作 目前是

以不賠錢 為第一 獲利放第2 目前做法就是

  • 不追熱門股 選股大多都是 死魚 沒成交量 低位階 為主
  • 然後用基本面 找到會成長的 股票會漲的原因很多 題材 籌碼 線型很多很多
  • 我則是專門找成長 選股所有的前提都是找到 EPS成長的股票
  • 搭配前面 所說 再低位階 跌無可跌的情況下 [再有限的損失中 去賭未來成長]

然後我也是人 不是100%都對 也很膽小 重壓看錯 被咬到也很痛(親身經驗) 所以把持股打超散 大多數的人是 買個3.4隻

  • 我是選40隻裡 只要有21隻賺錢 19隻賠錢 我整體就賺錢了!!

  • 目前想買的還是很多 現在的錢 還不夠把想要的都買下來QAQ

  • 然後 每季財報 每月營收 開始對答案 要是不如預期 馬上就會換掉 (想買的股票一堆還在排隊) 好的就留著 等待下次 營收 季報 或是發現更好的 也會從持股中 選一個可能比他爛的換掉(有時反而換了更差) 每次大量交易 通常都是 營收 季報開獎那半個月

建議 對於想走我這派的 財報要會看 會看財報 才能精準估EPS 還有就是要有興趣 我是當娛樂再找股票的 如果當工作來找 大概會很痛苦吧 必進這是很枯燥乏味的事 還有就是明明研究了 卻沒買不是很痛苦嘛!! 所以就越買越多了^_^ 買很多的好處 就是能大量累積經驗值 我以前地雷也是踩過很多的 QAQ 都是血與淚堆起來的經驗

反市場 JG股市操作原理

出處: https://stu15834.pixnet.net/blog/post/352668034

image

image

  • J派核心原理 – 反市場

  • 世界上存在許多可遵循的重要規則,如看見紅燈停下,看見綠燈才起步

    • 「人們規定」的常識,這些規則可以看到多數人興奮和恐懼時,便是可以利用的東西。
  • 範例 逆KD用法

    • 出現上升趨勢時
    • 在KD向下交叉且股價下跌時買進
    • 以前波低點作為停損
    • 當KD向下交叉失敗又突破前高時,將這次反轉失敗低點作為停損

停損

  • 買進後續走跌並達到心理停損點 (感到壓力)

  • 跌破確認過的低點

  • 範例 逆布林+極限加碼

精神

  • 抓住群眾恐懼時 – 在恐懼時買進

  • 只管買進,除非暴利不然不停立

操作

  • 在出現上升趨勢時

  • 在布林下緣位置買進

  • 突破上緣後不設技術分析停利,直到滿意報酬才停利

  • 每漲250點(期貨3%)或股價10%等比買進第二筆

  • 布林中線(20MA)上彎,同上每250點依承受風報比投入3.4筆

進場後操作

  • 若非勝率高過9成,要遵守有賺不能賠

  • 好的買點買進後都會漲一下

    • 沒漲一下 : 買點錯了,立即出場
    • 彈一下跌回 : 量能弱應退出
    • 彈後向上 : 進入移動停利操作,向上加碼
    • 彈後噴出 : 以K棒低點移停,根據風報比加碼
    • 進場後股價在停損停利間遊移 :
      • 賭博式停利法(目前最高價到停損價的一半停利,賭大獲利)
      • 進場時應看該股過去上漲時回檔%數,看報酬率能否承受
  • 一口單

    • 拿過去一週的線圖找出事後看好的出手點,將這個出手點依據套用到過去一段時間看是否也完美接下來一段時間只在看到這個依據時才場,不然不動作
  • 回測

    • 過去不代表未來,關鍵在於建立信心,看看自己當下能否做出正確的決策
    • ex: 線圖網站 – Trading View、Stock-ai
    • 回測要把握三個方向的表現
    • 多頭、空頭、盤整

客製化交易SOP四步驟

  • 目標設定 : 放大心中報酬率,待找出最大痛苦值來制定報酬率

  • 回測技術: 找出適合反市場的武器。

    • 用一樣技術的人哪裡會興奮? 哪裡會恐懼? 獨立思考這些事)
  • 根據風險報酬比設定停損停利和分批計畫

  • 下一口單,寫交易日至,隨時調整SOP

  • 如何設定目標獲利 : 先回測過去暴漲型態前會出現的跡象,操作就是跡象出現時如何去賺到這筆錢,當找出過去暴漲跡象時就可得出風險報酬比,但漲跌前跡象其實差不多,所以只挑選一個方向做,猜對照計畫加碼,猜錯依風險報酬比停損。

JG八原則

  1. 股票市場就是賭 : 承認運氣的重要性,運氣不對就離開

  2. 務必和股市預言保持距離 : 預測應著重預測過程的「資料」而非結果

  3. 用財報選股離暴賺太遠

    1. 財報 – 過去式
    2. 買點 – 技術面
    3. 持有 – 想像力
  4. 暴賺是最健康的股市態度

    1. 不想暴賺的人容易短進短出,只有想大賺的人懂得持有
    2. 兩種人在挑選股票的方向和心態會有差異
  5. 當然要知道輸家的下一步

    1. 在輸家最容易恐慌的地方,便是最安全的地方
    2. 輸家的特徵
      1. 沒耐心 → 持有時間要長
      2. 短線容易恐慌 → 只在恐慌時進場
      3. 喜歡當沖 → 絕不用市價單,要習慣掛低兩檔買進。
      4. 盤中短線振幅越大代表短線客停損造成波動,不該進場
  6. 「優勢」為輸贏最大分水嶺

    1. 找到自己可用的優勢
      1. 能否盯盤累積盤感
      2. 有無產業背景
      3. 人脈充足?有無內線可用?
  7. 風險報酬比

    1. 股市賺錢不是靠預測,而是在最好的買點買進(符合風險報酬比)
    • 風報比的觀念在於好的防守,把防守當成最好的進攻
  8. 要賺一輩子,一定要有全面性的操盤力

    1. 每個人都有心理極限,即使靠技術賺錢了,在達到極限後成長會減緩甚至賠錢。
    • 基本面和技術面雙能力才能讓交易長久,技術找買點,基本找信心。

日本股神CIS研究會

只要待在股市一天,手上的現金就是力量。 愈是靠投資賺錢的人,花在投資以外的錢就要更小心謹慎。 要是沒了本金,就無法賺大錢,也無法採取比較強勢的策略。 我個人認為最有效率的投資方法是找出能大撈一筆的機會,盡可能把全部財產都投進去。 所以我才會鎖定股價波動比較劇烈的個股,小心翼翼的保管著可以孤注一擲的資金。 我認為效率就是一切,絕對不會像他那樣。就我的判斷來說,他大概有20~25%的機率會被股票市場洗出去。

從小手川隆來思考乖離率

出處: https://vocus.cc/article/608f9e22fd8978000194b374

當初月光在看過CIS所寫:主力的思維這本書後,對於當沖這個交易法,有了大概的印象。而具體的操作方式,要另外買專書來學習。而作者另外提到的好友或是師父?是B.N.F,是比他更厲害的超級散戶,個人資產來到二百多億日圓。他的本名是小手川隆,從本金160萬日圓開始發跡。

他所崇拜的偶像是美國知名投資人尼德厚夫,針對這位大師的書,目前只買的到尼德厚夫投機術上下冊這套書。這位大師經過破產後從新出發,想必有許多體悟,寫成此書也應有參考性。 至少有書可以參考就很不錯了,因為小手兄本人並沒有出書,因為完全不缺錢,也不想要出名,自然沒有出書的必要。缺點就是無法讓好奇的大眾瞭解他的投資方法為何。幸好,小手兄有接受電視臺訪問,留下一些資料給大眾去揣摩。

首先可以瞭解到的是,小手兄是透過每日的大量交易來快速提升自己的資產。他自己也坦言,無時無刻都在思考投資的策略,甚至連吃飯都可以盡量簡單,旅遊也不想去,真正做到超級專業投資人的境界!已非常人可以達到的! 而想要在臺灣從事這樣大量頻繁的交易,光成本就不划算,因此一開始就無法複製這樣的操作手法。但即使如此,提高勝率的方法還是可以學習的,對於全球的消息面掌握度要夠,如此才能在每次下單時掌握先機。

一般散戶投資人常常是隔天甚至隔兩天才知道消息,此時再進場操作已經來不及了。真正是錢有四隻腳,追也追不上。而要做那麼多筆交易,就必須要盡快的掌握最新的消息。任何的風吹草動,都可能是造成價格波動的原因。

有專家認為小手兄是採取所謂的逆乖離買法。這是一種反市場投資法嗎?看起來就像是高點時賣出,低點時買入。乖離率本來就是判斷進出場時點的指標,當負乖離率高的時候,就可以考慮進場,小手兄認為這樣的勝率比較大。

甚至也可以說,買低不買高,才可以快速的賣出獲利。而且因為要快速的進行多筆交易,因此要保持冷靜,要不帶感情的做出很多判斷。所以也有專家也主張就交給程式來就好,因為是機械式的操作,程式就是沒有感情的操作。仔細思考小手兄的交易法,發現實際上,上班族無法作到這種強度的交易頻率,況且臺灣也沒有像日本這麼低的稅率。因此,頂多隻能作短日期的交易,例如以持股一至兩周為目標,沒賺到或虧損都得要出場。

而所謂順勢交易,也要看能否買在起漲點,而獲取較大的勝率。因為如果把錢投注在漲最多的,風險過大。上漲的標的這麼多,總不用去挑最多人的,還有很多其他的可以挑,也可以避免被主力倒貨。

的確,順勢交易有很多好處,而長期持有的價值投資,在短期看來獲利不佳。**一般散戶透過基本面的分析資訊,看來也落後在那些強大的法人分析報告之後。人家都研究透徹了,也已反應在價格上,當然不容易有獲利機會,只能等待發生壞事情時再大量買入。**而且,像小手兄這種持續的高強度交易,有幾人撐得住?整天都想著交易的事情,其他的事情可以全都拋下不管嗎?或許對小手兄而言,那些生活瑣事反而比交易還要麻煩許多。

對月光來說,獲利方法百百種,一定要選這麼累的方式嗎?但即使如此,小手兄的交易策略還是帶來許多不一樣的思考衝擊。至少在選股時,可以多參考乖離率作為進出場的輔助標準。月光在網路看到一則廣告說,當沖為交易之母,因為可以在短短一兩天內,看到許多走勢的變化,並且試著從中獲利。這段話或許有些誇大,那不會當沖交易的投資人不就無法成為投資大師?

月光覺得,透過小手兄樸實無華的交易生活,讓自己見識到,真的有人可以因此而成功,只要先能夠把交易擺在生命中的第一位,全心全力的不斷執行交易,就有可能達到。真能做到每日幾個億的交易量,那還有什麼好害怕的呢?


  • BNF 操作
    • 25日爆量V轉點
    • 做多 等待型: 只做爆量不跌
    • 做多 等待型: 只做V轉守今低停損

CIS的投資哲學

https://zhuanlan.zhihu.com/p/368672327

今天分享一本國內影響力不大,但是我覺得裡面的內容真的是直擊交易本質的。該書封面標題是【憑一己之力左右日經指數的男人的投資哲學】,起這個標題我覺得稍微有點太用力,可能是需要考慮到書籍的銷售方面。如果可以的話,我覺得作者更想起一個【CIS的投資哲學】這樣的標題。因為不管是作者本身的風格,或者說以短線交易的態度來說,後面這個標題更直擊本質。很多人說CIS是日本或者是當代最像利弗莫爾的人,這個說法且本身沒有一個衡量標準,而且很多偉大的交易員都是十分低調的,除了CIS還有BNF等交易員如果不是因為J-COM事件可能到現在還是不為人所知。但是我認為如果拿兩者的書籍相比的話(股票大作手回憶錄),我很難判斷出哪本書對於我來說更喜歡。股票大作手回憶錄影響力毋庸置疑,但僅有一百多頁的CIS的書,更簡單精要,關鍵點突出,一下就點破了很多至關重要的內容。我在讀完CIS後,我又想馬上拿起來再認真看一遍,因為實在太精彩了,裡面的一些關鍵點要是我能早點看到的話,可能能節約很多我在市場學習的時間。我在網上評價看到這本書的評分並不高,不乏有很多人給出一星和三星的差評,還有人評論“這是我讀過最沒用的投資書了”。這只是視角問題,大眾更傾向於獲取簡單、能直接操作的技巧,或者是更多找到共鳴的經歷故事。

按照作者的性格,本是不會去做寫書這樣費力又得不到應有樂趣的事,這本書的問世也是因為他的一個麻將朋友鼓勵、支援下才得以與我們見面。作者在書中寫到“我的投資方式比較類似電玩玩家或賭徒,而我真的是個遊戲玩家,也嘗試過賭博,所以判斷股票行情對我來說,就像是一場遊戲(賭博)”,CIS是個不折不扣的遊戲迷,和一般遊戲迷不一樣,他對(單場)勝利本身不感興趣,他執著於弄懂遊戲的獲勝邏輯、獲勝原則。從小便在玩遊戲中一直獲得比較好的成績,再往後,規則複雜點的麻將,股票,就成了CIS的熱衷。開始時CIS也不是就馬上學習到市場的正確方向,和大多數人一樣,他是從基本面開始投資股票的,通過市值和營收來決定買入的股票。但是這個情況持續了兩年半左右,陸續虧了一千萬日元。他意識到他走錯方向了,從一次交流會得到啟發後,確定了自己正確的短線交易風格。

CIS在書中提到數個交易原則,這些原則一個一個相互串聯,沒有什麼祕訣,但是祕訣卻是所有的細節組合起來,方有可能成功。以下是書中提到的部分重要原則,我在這裡簡單地展開說明:

1.“順勢操作,勝算最高”

順勢操作是做交易的人聽到最多的一句話,但是理解起來卻五花八門。持續上漲的股票會繼續上漲,持續下跌的股票會再跌,你無需知道背後的原因是什麼,你也無從得知,但是股價現在處於上漲的時期中,必定是大部分人和資本在進行買進操作,才會推升股價的上行。所以順著市場趨勢操作,勝算最高。但是需要注意的是,操作等級的問題,你是以什麼樣的週期來操作的,這點你必須自己明確。

2.“真正的隨機,比你以為的還要殘酷”

大多數人的腦子裡對隨機、平均的概念不夠貼近現實。什麼是現實,現實就是投十次硬幣,八次正面朝上的情況時常發生,‘概念上的隨機,多半給人分散得很平均的印象,但是從微觀角度來看,其實很容易側重一方’。CIS舉例打麻將,你聽很多張牌,但是對家只聽一張牌,但是被對家自摸了,這種被機率背叛的狀況要多少有多少。解決這個問題,只能將自己的視角從本能的直覺中糾正過來,以一個更宏觀的視角來看待結果的隨機性。

3.“基本上請不要預設立場,上漲的時候就要勇敢進場”

這個點作者沒有專門設定一個小標題來說明,但是我覺得也很重要。一旦你有立場,對市場有了主觀預期,無論是交易計畫,還是止損,你都沒辦法很好地執行。“我通常不在意微幅的波動,而是下跌到一定程度才賣掉”,這也是不預設立場的表現。

4.“千萬不能在回呼時買進”

CIS在文中表示回呼時買進屬於逆勢操作,認為上漲的股票會繼續上漲,基本上就要立刻買進。“預測反轉的時機或價格,只是一廂情願的揣測”。關於這點我不能做太多的說明,我說明的太多了很容易造成誤會,需要自己去理解。

5.“只顧著止盈會錯失大波段”

人的本能會在有盈利的時候傾向於止盈,而浮虧的時候不願意承認虧損。當有盈利的時候應該抱著你的頭寸,直到行情出現改變,在趨勢向上的時候止盈多單,絕不是聰明的方法,這跟順勢操作是同一個理念。

6.“重點不是勝率,而是加總的損益——有沒有這種概念,是能不能靠股票賺錢的關鍵”

上面這句話非常關鍵,如果你什麼時候能在心底真正品嚐出這句話的含義,那你就有了盈利的最底層基礎,以一個更接近真實的眼光來看待機率,並突破“隨機”,盈利在向你招手。CIS統計自己的操作下來,單筆勝率只有30%左右,但是都是小賠收場,賺到的金額是小賠的十倍、二十倍,所以這就是以整體的眼光來看待市場,才能突破盈虧平衡。

  1. “攤平是最差勁的買法”

這點沒什麼好過多解釋的,攤平是知道錯了還增加賭金,與順勢的大原則背道而馳。玩股票最重要的是迅速停損,必須勇於認錯,儘可能把損失控制在最小的範圍內。

  1. “已經停損的股票,再次上漲時可以買進嗎”

“首先止損的那一刻承認了自己的失敗,然後漲到比自己賣出的金額更高的價位買回,承認止損也是錯誤的決定,或許有人不願意承認兩次錯誤,但我不在乎面子問題,總是可以心平靜氣的操作。我不會把每次的買賣視為勝負,所以不在乎”。單次或單天的盈虧毫無意義,當你能以一個系統的眼光來看待盈虧時,才能走得平穩。但是凡事都有例外,要是連續止損三次以上怎麼辦,CIS給出了他的答案“要是同一隻股票買賣三次以上都看走眼,我也會承認自己看不懂那隻股票,決定不陷入泥沼,就此收手,但我不會放在心上,繼續操作下一隻股票。拘泥局部的勝負,一點意義都沒有”。

9. “計算幾賺幾賠,沒有意義”

“很多人都說賭博時,見好就收很重要,這個說法,與只顧止盈的心態大同小異。考慮到注意力及體力,的確要見好就收,但是如果有勝算,繼續撐下去可以越贏越多。之所以想要見好就收,不過是出於凡事都會取得平衡的想法”。這個話題非常有意思,時常會有人說,他當天無論盈虧10%,就此罷手,不做了。那這麼做到底有沒有用呢?我的建議是,如果你是人工交易,那麼可以這樣設定一個每天的上限值。可能有的人會說可能你正好止損到一天的規定,行情就來了呢,是有這種情況,但是相反在盈利方面也是一樣的(賺到了規定值後又回吐利潤),所以以這個角度來看是一個對稱,但是這個問題不是這麼簡單,因為討論起來有太多內容的,在此不詳細探究。那我為什麼建議這樣做,人工交易往往會出現情緒的波動和注意力、體力消耗,即使當天內的潛在盈虧振幅被你抹掉了不少,但是長期下來你還是走在上破路的,走得更平滑。

10. “不願認賠的心情,會輸得一敗塗地”

形勢不利時,迅速止損,比起技巧,更接近心態。CIS書中舉例,他曾經因為太快止損而錯過了接下來的大行情,但是他卻毫不在意。一旦察覺到不對勁,無論結果如何,應該馬上賣出,他雖然結果上來說止損太早,但是以心態而言,他認為自己沒做錯。這就是一個頂級操盤手的操盤守則,永遠不以單次的結果論輸贏,將自己思維層面提高到與市場真正同頻的“道”上。

沒錯,真實的頂級操盤手的原則就是這麼簡單的幾條,並沒有什麼複雜的操作技術,當你真正地把這些市場哲學理解、融會貫通後,會發現基本上任何簡單技術都是可以賺錢的,能通過觀察市場學到最適合你的技術。但是每一條都需要你以最大的熱情來進行思考、領悟,信心的建立過程不是三天兩天就能完成得,真實的操作水平取決於你理論的理解深度,這個過程是漫長的,需要一個人最深處激發出的堅持和認真才有可能成功。

這些最佳化方法比起暴力搜尋可以讓策略更加robust

基因演算法 : https://medium.com/geekculture/optimising-trading-strategies-by-using-a-genetic-algorithm-bc90d7ddbefd

向前優化 : https://algotrading101.com/learn/walk-forward-optimization/

回到半年多前樓主的原帖稍微總結一下:
1. 基因演算法在優化投資策略(即「選股」、「擇時」)確實是很重要且相關的。
2. 基因演算法其實算「最佳化」的方法,所以什麼「模擬退火」、「粒子群(其實是鳥群和魚群)」、「梯次下降」等都是,而「線性規劃」、「二次規劃」、「牛頓法」也是,「向前優化」其實也是,族繁不及備載。基因是「演化類」,常見的是 GA (單目標), NSGA-II (多目標), NSGA-III (很多目標)等。
3. Danny Grove 在一年半前(西洋方格子)的文章,最早提出了使用基因演算法來優化策略的觀點,經過我們Finlab-er的實踐,特別是 TNeagle 在半年前率先使用,到現在我們使用的程度已經進化很多了。
4. 這種進化之所以可能,其實是奠基在韓老師建立的Finlab 資料和API以及很多個策略範例上,開創新的道路通常都是最難的。
5. 世界上的主流討論,看起來還是機器學習上面,包括討論少量參數(節儉原則)、大量參數甚至巨量參數跑出的效能。但我對機器學習在開發策略的成效如何還有待了解。
6. 目前 QuantCon 竟然沒有亞洲的場次,連中國、印度、新加坡也沒有,只偏重在歐美,是不是亞洲的量化人口還不夠多?
7. 至於向前優化部分,因為要切資料集,我也沒去研究這一塊;提出者是 algotrading101 的 Lucas Liew,只能再看有沒有進展。
量化投資其實很有趣的。
其實基因演算法就是適者生存,如果有學過生物的"育種"會更瞭解這個概念。
簡單說把策略視為個體,選股條件就是基因
一個好策略,一定有好基因
所以從一堆策略,篩選出比較好的策略後進行"雜交"
ex.
甲策略:A、B、C、D
乙策略:E、F、G、H

交配做出甲乙、乙甲策略
甲乙:A、B、G、H
乙甲:E、F、C、D

然後重複篩選和雜交的動作,就可以得到一群有著不錯基因的策略。
展開來講真的可以寫一大篇,核心概念就是上面所說的 "適者生存"。

結論:
這個方法不一定能找到 "最好" 的策略,但是可以找到 "比較好"的策略
前提是有辦法定義什麼是"好策略"

股市的自然選擇:會進化的策略|FinLab 讀書會

https://discord.com/channels/897386624067969034/1123838318027755581/1193378774163669103

https://www.youtube.com/live/W-jxEZXu-pQ

我對於"策略的可解釋性"一直不理解為什麼要執著於這件事。首先,人類不是全知全能的。 當人類不是全知全能,就一定有不可解釋的事情,但這並不代表不可解釋的事情不合理,而是目前我們無法解讀。 我PPT上面才會有"存在即合理"這一句話。 其實目前大部分的AI,複雜性都已經超出人類可以解釋的範圍,但這並不代表AI給出的答案不合理(這也是有些人會認為AI有智慧的原因)

再來,GA會執著於某個數字,通常代表那邊就是平衡點。是"目前"基於歷史數據的平衡點,在數據的積累以及時間流動下這個平衡點是會"變動"的。 <@284659625175941121> 說 "感覺是在解空間中硬畫出一條線來擬合訓練資料" 。 其實這一點就是機器學習的目標,只是我們要找到的是"擬合的這條線"和"市場的隨機性"中取得最佳的平衡。 這樣才不會出現"過擬合" 而基因演算法從底層邏輯上就很難過擬合,所以可以不太去關注過擬合這個議題

ex. close.average(20) > close.average(60) 這樣一個條件在機器學習裡面就是2個參數 n種組合。 這樣的解空間真正是無限大,雖然直接限縮解空間範圍,但只要選股條件數量及品質不錯,也可以找到不錯的解。

我自己其實是在研究機器學習的過程中,發現機器學習的難度太大,短時間很難有一個不錯的成果。 所以我抽取機器學習的概念從邏輯底層去找一個簡單好用的替代辦法,所以才有分享的內容。 機器學習: 輸入許多特徵資料=>調參=>建立模型

基因演算法: 利用人類已經研究多年的選股條件將特徵資料先進行預先處理=>簡化調參=>建立模型

簡單來說,這一套就是機器學習的下位替代版。 只是因為引入很多人類已經研究過的經驗,所以可以在初期表現比機器學習好。 其實對我們來說,我們沒有必要去找到最佳的那個解,只要找到"相對"不錯的解然後可以在股市裡賺錢就好。 至於可解釋性,看有沒有經濟學家要拿去寫論文吧。

年化報酬率/abs(MDD) * 日索提諾

GA + Vectotbt

https://www.youtube.com/watch?v=W-jxEZXu-pQ

import numpy as np
import yfinance as yf
import multiprocessing
import time
import vectorbt as vbt
import warnings
from deap import base, creator, tools, algorithms

warnings.filterwarnings("ignore")


def run_strategy_bt(data, fast_ma, slow_ma):
    fast_ma = vbt.MA.run(data, fast_ma)
    slow_ma = vbt.MA.run(data, slow_ma)
    entries = fast_ma.ma_crossed_above(slow_ma)
    exits = fast_ma.ma_crossed_below(slow_ma)
    return vbt.Portfolio.from_signals(data, entries, exits, init_cash=100)


# 定義適應度函數
def trade_fitness(individual, data):
    sma1, sma2 = individual
    if sma1 < sma2:
        fast_ma = sma1
        slow_ma = sma2
    else:
        fast_ma = sma2
        slow_ma = sma1

    pf = run_strategy_bt(data, fast_ma, slow_ma)
    returns_stats = pf.returns_stats(column=10, settings=dict(freq="d"))
    Annualized_Return = returns_stats["Annualized Return [%]"]
    Max_Drawdown = returns_stats["Max Drawdown [%]"]
    Sortino_Ratio = returns_stats["Sortino Ratio"]
    # print(Annualized_Return)
    # print(Max_Drawdown)
    # print(Sortino_Ratio)
    fitness = (Annualized_Return / abs(Max_Drawdown)) * Sortino_Ratio
    if np.isnan(fitness):
        fitness = 0

    print(
        f"pid: {multiprocessing.current_process().pid}, individual: {individual}, fitness: {fitness}"
    )
    return (fitness,)


if __name__ == "__main__":
    # 下載股票歷史數據
    ticker = "AAPL"
    data = yf.download(ticker, start="2020-01-01", end="2023-04-30")
    # fast_ma = vbt.MA.run(data, 10)
    # slow_ma = vbt.MA.run(data, 50)
    # entries = fast_ma.ma_crossed_above(slow_ma)
    # exits = fast_ma.ma_crossed_below(slow_ma)
    # pf = vbt.Portfolio.from_signals(data, entries, exits, init_cash=100)
    # returns_stats = pf.returns_stats(column=10, settings=dict(freq="d"))
    # print(data.head())
    # input()
    # 設置基因演算法參數
    creator.create("FitnessMax", base.Fitness, weights=(1.0,))
    creator.create("Individual", list, fitness=creator.FitnessMax)

    """
    在這個示例中,我們首先定義了交易策略的適應度函數 trade_fitness。這個函數計算了四個移動平均線的值,並根據它們的交叉關係生成交易訊號。然後,它計算了基於這個交易訊號的策略收益。適應度函數的輸出是策略的總收益。
    接下來,我們設置了基因演算法的參數,包括個體表示、交叉、變異、選擇等操作。個體由四個整數表示,分別對應四個移動平均線的天數,範圍為 5 到 250 天。
    然後,我們執行基因演算法優化,輸出了最優解及其適應度值(總收益)。
    運行這個程式碼,您將獲得最佳的四個移動平均線天數,以及使用這些天數時的最大策略收益。
    您可以根據需要調整適應度函數、個體表示等,以找到更複雜的交易策略。此外,您還可以添加其他約束條件,例如最大回撤等,使得優化更加全面。
    """
    toolbox = base.Toolbox()
    toolbox.register("attr_int", np.random.randint, 5, 100)

    toolbox.register(
        "individual", tools.initRepeat, creator.Individual, toolbox.attr_int, n=2
    )
    toolbox.register("population", tools.initRepeat, list, toolbox.individual)
    toolbox.register("evaluate", trade_fitness, data=data["Close"])
    toolbox.register("mate", tools.cxTwoPoint)
    toolbox.register("mutate", tools.mutUniformInt, low=5, up=100, indpb=0.2)
    toolbox.register("select", tools.selTournament, tournsize=3)

    """
    創建一個進程池,裡面有8個工作進程
    使用toolbox.register("map", pool.map)將pool.map函數註冊為toolbox中的"map"函數。
    這樣在評估個體時,就會使用pool.map進行並行計算。
    在優化結束後,使用pool.close()關閉進程池,並使用pool.join()等待所有工作進程結束。
    """
    pool = multiprocessing.Pool(processes=8)
    # 使用toolbox.register將pool.map註冊為"map"函數
    toolbox.register("map", pool.map)

    # 執行基因演算法優化
    pop = toolbox.population(n=100)
    hof = tools.HallOfFame(1)
    stats = tools.Statistics(lambda ind: ind.fitness.values)
    stats.register("avg", np.mean)
    stats.register("std", np.std)
    stats.register("min", np.min)
    stats.register("max", np.max)

    start_time = time.time()
    pop, log = algorithms.eaSimple(
        pop,
        toolbox,
        cxpb=0.5,
        mutpb=0.2,
        ngen=100,
        stats=stats,
        halloffame=hof,
        verbose=True,
    )

    # 關閉進程池並等待所有結果
    pool.close()
    pool.join()
    end_time = time.time()
    print("Time taken: ", end_time - start_time)

    # 輸出最優解
    best_ind = hof.items[0]
    print("Best individual: ", best_ind)
    print("Best fitness: ", best_ind.fitness.values[0])

    sma1, sma2 = best_ind
    if sma1 < sma2:
        fast_ma = sma1
        slow_ma = sma2
    else:
        fast_ma = sma2
        slow_ma = sma1

    pf = run_strategy_bt(data["Close"], fast_ma, slow_ma)
    returns_stats = pf.returns_stats(column=10, settings=dict(freq="d"))
    print(returns_stats)
    stats = pf.stats(column=10, settings=dict(freq="d"))
    print(stats)

GA_Six_FA_Finlab

from finlab import data
from finlab.backtest import sim
from functools import reduce
from deap import base, creator, tools
import json
import finlab
import numpy as np
import pandas as pd
import random
import multiprocessing
import warnings

warnings.filterwarnings("ignore")


def eval(index_list, df_list, eval_dict):
    # Cache:
    id = "".join([str(value) for value in index_list])
    if id in eval_dict:
        return eval_dict[id]
    else:
        # Filter the MyClass instances which are selected (index == 1)
        selected_dfs = [df for index, df in zip(index_list, df_list) if index == 1]

        # If no DataFrames are selected, return an empty DataFrame
        if not selected_dfs:
            result = pd.DataFrame()
        else:
            # Perform the intersection operation on the selected DataFrames using reduce
            result = reduce(lambda x, y: x & y, selected_dfs)

        if result.empty:
            value = 0
        else:
            # 年化報酬率 (daily_mean)
            # 年化夏普率 (daily_sharpe)
            # 年化所提諾率 (daily_sortino)
            # 最大回撤率 (max_drawdown)
            report = sim(result, resample="W", market="TW_STOCK", upload=False)
            日索提諾 = report.get_stats()["daily_sortino"]
            max_drawdown = report.get_stats()["max_drawdown"]
            daily_mean = report.get_stats()["daily_mean"]
            print(f"日索提諾: {日索提諾}")
            print(f"最大回撤: {max_drawdown}")
            print(f"年報酬: {daily_mean}")
            value = daily_mean / abs(max_drawdown) * 日索提諾
            print(f"fitness value: {value}")
            # metrics = report.get_metrics()
            # value = (
            #    metrics["profitability"]["annualReturn"]
            #    / abs(metrics["risk"]["maxDrawdown"])
            # ) * metrics["ratio"]["sortinoRatio"]

        eval_dict[id] = (value,)
        return (value,)  # Return a tuple


def main(Population_size, Generation_num, Threshold, df_list):
    # Initialization
    Cond_num = len(df_list)

    # Creator
    creator.create("FitnessMax", base.Fitness, weights=(1.0,))
    creator.create("Individual", list, fitness=creator.FitnessMax)

    # Toolbox
    toolbox = base.Toolbox()
    # Attribute generator
    toolbox.register("attr_bool", random.randint, 0, 1)
    # Structure initializers
    toolbox.register(
        "individual", tools.initRepeat, creator.Individual, toolbox.attr_bool, Cond_num
    )
    toolbox.register("population", tools.initRepeat, list, toolbox.individual)

    # Operators
    num_cpus = multiprocessing.cpu_count() - 1
    pool = multiprocessing.Pool(processes=num_cpus)
    toolbox.register("map", pool.imap)

    # Use Manager to create a shared dictionary
    manager = multiprocessing.Manager()
    eval_dict = manager.dict()

    # Pass eval_dict to the evaluate function
    toolbox.register("evaluate", eval, df_list=df_list, eval_dict=eval_dict)
    # 交配
    # cxOnePoint: 隨機選取一個點,將這個點之後的基因交換
    # cxTwoPoint: 隨機選取兩個點,將這兩個點之間的基因交換
    # cxUniform: 對每個基因以 indpb 的機率進行交換
    toolbox.register("mate", tools.cxTwoPoint)
    # 變異
    # mutFlipBit: 對每個位元以 indpb 的機率進行反轉 0->1, 1->0
    toolbox.register("mutate", tools.mutFlipBit, indpb=0.05)
    # 選擇
    # selTournament: 隨機選取 tournsize 個個體,再從這 tournsize 個個體中選擇適應度最好的個體
    # SelRoulette: 依照適應度的比例選擇個體
    # selBest: 選擇適應度最好的個體
    toolbox.register("select", tools.selTournament, tournsize=3)

    pop = toolbox.population(n=Population_size)

    # Evaluate the initial population
    fitnesses = list(toolbox.map(toolbox.evaluate, pop))
    for ind, fit in zip(pop, fitnesses):
        ind.fitness.values = fit

    CXPB, MUTPB = 0.5, 0.2
    g = 0

    # Initialize fits list
    fits = [ind.fitness.values[0] for ind in pop]

    while max(fits) < Threshold and g < Generation_num:
        g += 1
        print("-- Generation %i --" % g)

        offspring = toolbox.select(pop, len(pop))
        offspring = list(pool.imap(toolbox.clone, offspring))

        for child1, child2 in zip(offspring[::2], offspring[1::2]):
            if random.random() < CXPB:
                toolbox.mate(child1, child2)
                del child1.fitness.values
                del child2.fitness.values

        for mutant in offspring:
            if random.random() < MUTPB:
                toolbox.mutate(mutant)
                del mutant.fitness.values

        invalid_ind = [ind for ind in offspring if not ind.fitness.valid]
        fitnesses = list(toolbox.map(toolbox.evaluate, invalid_ind))

        for ind, fit in zip(invalid_ind, fitnesses):
            ind.fitness.values = fit

        pop[:] = offspring

        fits = [ind.fitness.values[0] for ind in pop]

        length = len(pop)
        mean = sum(fits) / length
        sum2 = sum(x * x for x in fits)
        std = abs(sum2 / length - mean**2) ** 0.5

        print("  Min %s" % min(fits))
        print("  Max %s" % max(fits))
        print("  Avg %s" % mean)
        print("  Std %s" % std)

    return dict(eval_dict)


if __name__ == "__main__":
    finlab.login(
        ""
    )

    # Data preparation
    close = data.get("price:收盤價")
    monthly_revenue_grow = data.get("monthly_revenue:去年同月增減(%)").fillna(0)
    operating_profit = data.get("fundamental_features:營業利益率")
    net_income_grow = data.get("fundamental_features:稅後淨利成長率")
    eps = data.get("financial_statement:每股盈餘")
    stock = data.get("financial_statement:存貨")
    stock_turnover = data.get("fundamental_features:存貨週轉率").replace(
        [np.nan, np.inf], 0
    )
    season_revenue = data.get("financial_statement:營業收入淨額")
    fcf_in = data.get("financial_statement:投資活動之淨現金流入_流出")
    fcf_out = data.get("financial_statement:營業活動之淨現金流入_流出")

    # Processing
    # Revenue growth
    monthly_revenue_grow_aa = (
        ((monthly_revenue_grow > 0).sustain(6))
        & (monthly_revenue_grow.average(6) > 25)
        & (monthly_revenue_grow.rise())
    )
    # Profit rate
    operating_profit_stable = (
        ((operating_profit.diff() / operating_profit.shift().abs()).rolling(3).min())
        * 100
    ) > -20
    operating_profit_aa_1 = operating_profit_stable & (operating_profit.average(4) > 15)
    operating_profit_aa_2 = operating_profit_stable & (
        (operating_profit.average(4) <= 15)
        & ((operating_profit.average(4) > 10))
        & operating_profit.rise()
    )
    operating_profit_aa = operating_profit_aa_1 | operating_profit_aa_2
    # Profit growth
    net_income_grow_aa_1 = ((net_income_grow > 0).sustain(3)) & (net_income_grow.rise())
    net_income_grow_aa_2 = (net_income_grow > 50).sustain(3)
    net_income_grow_aa = net_income_grow_aa_1 | net_income_grow_aa_2
    # Profit strength
    eps_aa = (eps.rolling(4).sum() > 5) & (eps > 0)
    # Inventory turnover
    stock_low = (
        (stock_turnover <= 0)
        | (stock_turnover > 10)
        | ((stock / season_revenue) <= 0.04)
    )
    # stock_low = ~stock_low
    # stock_low = stock_low.apply(lambda x: -x)
    stock_turnover_stable = (
        stock_turnover.diff() / stock_turnover.shift().abs()
    ).rolling(3).min() * 100 > -20
    stock_turnover_cumulate_loss_gt_m20 = (stock_turnover.fall().sustain(3, 2)) & (
        stock_turnover.pct_change()[stock_turnover.pct_change() < 0].rolling(2).sum()
        * 100
        < -20
    )
    stock_turnover_aa = (
        # (~stock_low)
        stock_low.apply(lambda x: -x)
        & stock_turnover_stable
        & (stock_turnover.average(4) > 1.5)
        # & ~(stock_turnover_cumulate_loss_gt_m20)
        & stock_turnover_cumulate_loss_gt_m20.apply(lambda x: -x)
    )
    # Cash flow
    fcf = fcf_in + fcf_out
    fcf_aa = fcf.rolling(6).min() > 0

    # Moving average
    sma5 = close.average(5)
    sma20 = close.average(20)
    sma60 = close.average(60)
    sma240 = close.average(240)
    long_sma = (close > sma5) & (sma5 > sma20) & (sma20 > sma60) & (sma60 > sma240)

    df_list = [
        monthly_revenue_grow_aa,
        operating_profit_aa,
        net_income_grow_aa,
        eps_aa,
        stock_turnover_aa,
        fcf_aa,
        long_sma,
    ]

    # Example usage:
    Population_size = 30
    Generation_num = 5
    Threshold = 30

    result = main(Population_size, Generation_num, Threshold, df_list)
    print(json.dumps(result, indent=4))

基於遺傳演算法(deap)的配詞問題與deap框架

基於deap的程序框架

import numpy as np
from deap import base, tools, creator, algorithms
import random

# 問題定義;定式
creator.create('Fitness...', base.Fitness, weights=(...1.0,)) # 最大化問題
creator.create('Individual', list, fitness=creator.Fitness...)

# 個體編碼;主要依靠人為設計
geneLength=...
toolbox = base.Toolbox()
toolbox.register('個體實現方法的名稱',...)
toolbox.register('individual', tools.initRepeat, creator.Individual, toolbox.個體實現方法的名稱, n=geneLength)
#print(toolbox.individual())#列印檢查個體

#建立種群;定式
N_POP = ...#種群內個體數量,參數過小,則搜尋速度過慢
toolbox.register('population',tools.initRepeat,list,toolbox.individual)
pop = toolbox.population(n=...)#建立一個種群pop
#for ind in pop:#列印一個種群檢查
#   print(ind)

#評價函數;主要依靠人為設計
def evaluate(ind):
    pass
    
#註冊各種工具,人為選擇具體的方法;寫法定式
toolbox.register('evaluate', evaluate)#評價函數
toolbox.register('select', tools...)#選擇
toolbox.register('mate', tools...)#交叉
toolbox.register('mutate', tools...)#突變

#超參數設定;人為根據理論與經驗設定
NGEN = ...#迭代步數,參數過小,則在收斂之前就結束搜尋
CXPB = ...#交叉機率,參數過小,則族群不能有效更新
MUTPB = ...#突變機率,參數過小,則容易陷入局部最優

#開始工作,先對初始的種群計算適應度;定式
invalid_ind = [ind for ind in pop if not ind.fitness.valid]
fitnesses = toolbox.map(toolbox.evaluate,invalid_ind)
for ind ,fitness in zip(invalid_ind,fitnesses):
    ind.fitness.values = fitness
    
#循環迭代,近似定式,但是可以自行增加一些提高遺傳演算法性能的方法
for gen in range(NGEN):
    offspring = toolbox.select(pop,N_POP)#先選擇一次
    offspring = [toolbox.clone(_) for _ in offspring]#再克隆一次,克隆是必須的

    for ind1,ind2 in zip(offspring[::2],offspring[1::2]):#交叉操作
        if random.random() < CXPB:#交叉
            toolbox.mate(ind1,ind2)
            del ind1.fitness.values
            del ind2.fitness.values
    for ind in offspring:#變異操作
        if random.random() <MUTPB:
            toolbox.mutate(ind)
            del ind.fitness.values
    invalid_ind = [ind for ind in offspring if not ind.fitness.valid]#將刪除了適應度的個體重新評估
    fitnesses = toolbox.map(toolbox.evaluate, invalid_ind)
    for fitness, ind in zip(fitnesses, invalid_ind):
        ind.fitness.values = fitness
   #精英選擇策略,加速收斂 
    combinedPop = pop + offspring#將子代與父代結合起來
    pop = tools.selBest(combinedPop,N_POP)#再從子代與父代的結閤中選擇出適應度最高的一批作為新的種群

#顯示演算法運行結果
bestInd = tools.selBest(pop,1)[0]#選擇出最好的個體編號
bestFit = bestInd.fitness.values[0]#最好的個體適應度
print('best solution: '+str(bestInd))#列印解
print('best fitness: '+str(bestFit))#列印適應度

 由以上的程序框架可知:

  • (1)個體編碼方式、評價函數基本依靠個人根據實際問題設計。
  • (2)選擇方法註冊工具、迭代運算等基本是定式。
  • (3)迭代運算雖然可以當做定式用,但是適當增加一些諸如精英選擇策略這樣的程式碼可以明顯提高演算法性能。

配詞問題

  配詞問題內容比較簡單,比如有個字串‘woshidaxuesheng’,希望能用隨機演算法經過尋優將等長的隨機字母序列生成同樣的內容。在遺傳演算法裡就等於是生成一個擁有很多隨機字母個體的種群,經過運算後得到一個個體內容最接近於‘woshidaxuesheng’。   先貼上完整的原始碼,再分析問題。這裡可以看到配詞問題程式碼用了兩種評價函數:一種是將配詞問題轉化為最小化問題,令個體的內容與目標字串差異最小;一種是將配詞問題轉化為最大化問題,令個體的內容與目標字串相同性最強。   本程式碼將最小化思路程序片段進行了註釋,使用最大化思路程序。

import numpy as np
from deap import base, tools, creator, algorithms
import random

# 問題定義
#creator.create('FitnessMin', base.Fitness, weights=(-1.0,)) # 最小化問題
#creator.create('Individual', list, fitness=creator.FitnessMin)
creator.create('FitnessMax', base.Fitness, weights=(1.0,)) # 最大化問題
creator.create('Individual', list, fitness=creator.FitnessMax)

# 個體編碼
geneLength = 14#字串內有14個字元
toolbox = base.Toolbox()
toolbox.register('genASCII',random.randint, 97, 122)#英文小寫字符的ASCII碼範圍為97~122
toolbox.register('individual', tools.initRepeat, creator.Individual, toolbox.genASCII, n=geneLength)
#print(toolbox.individual())

#建立種群
N_POP = 100#種群內個體數量,參數過小,則搜尋速度過慢
toolbox.register('population',tools.initRepeat,list,toolbox.individual)
pop = toolbox.population(n=N_POP)
#for ind in pop:#列印一個種群檢查
#   print(ind)

#評價函數
#def evaluate(ind):
#    target = list('zzyshiwodedidi')
#    target = [ord(item) for item in target]
#    return (sum(np.abs(np.asarray(target) - np.asarray(ind)))),
def evaluate(ind):
    target = list('zzyshiwodedidi')#需要匹配的字串
    target = [ord(item) for item in target]#將字串內的字元都轉換成ASCII碼
    return (sum(np.abs(np.asarray(target) == np.asarray(ind)))),
    
#註冊各種工具
toolbox.register('evaluate', evaluate)#評價函數
toolbox.register('select', tools.selTournament, tournsize=2)#錦標賽選擇
toolbox.register('mate', tools.cxUniform, indpb=0.5)#均勻交叉
toolbox.register('mutate', tools.mutShuffleIndexes, indpb=0.5)#亂序突變

#超參數設定
NGEN = 300#迭代步數,參數過小,則在收斂之前就結束搜尋
CXPB = 0.8#交叉機率,參數過小,則族群不能有效更新
MUTPB = 0.2#突變機率,參數過小,則容易陷入局部最優

#開始工作,先對初始的種群計算適應度
invalid_ind = [ind for ind in pop if not ind.fitness.valid]
fitnesses = toolbox.map(toolbox.evaluate,invalid_ind)
for ind ,fitness in zip(invalid_ind,fitnesses):
    ind.fitness.values = fitness
    
#循環迭代
for gen in range(NGEN):
    offspring = toolbox.select(pop,N_POP)#先選擇一次
    offspring = [toolbox.clone(_) for _ in offspring]#再克隆一次

    for ind1,ind2 in zip(offspring[::2],offspring[1::2]):#交叉
        if random.random() < CXPB:#交叉
            toolbox.mate(ind1,ind2)
            del ind1.fitness.values
            del ind2.fitness.values
    for ind in offspring:#變異
        if random.random() <MUTPB:
            toolbox.mutate(ind)
            del ind.fitness.values
    invalid_ind = [ind for ind in offspring if not ind.fitness.valid]#將刪除了適應度的個體重新評估
    fitnesses = toolbox.map(toolbox.evaluate, invalid_ind)
    for fitness, ind in zip(fitnesses, invalid_ind):
        ind.fitness.values = fitness
   #精英選擇策略,加速收斂 
    combinedPop = pop + offspring#將子代與父代結合起來
    pop = tools.selBest(combinedPop,N_POP)#再從子代與父代的結閤中選擇出適應度最高的一批作為新的種群

bestInd = tools.selBest(pop,1)[0]#選擇出最好的個體編號
bestFit = bestInd.fitness.values[0]#最好的個體適應度
bestInd = [chr(item) for item in bestInd]#將該個體裡的ASCII碼轉換成字元形式
print('best solution: '+str(bestInd))#列印字元
print('best fitness: '+str(bestFit))#列印適應度,這裡可以看到適應度並不是我們需要的問題的解,僅僅是對解的一種評估得分

程式設計

個體設計

首先考慮個體是如何設計編碼的。

我們要得到一個與’zzyshiwodedidi‘最相近的字串,顯然這個個體也得是個小寫字母的字串,而且長度得和’zzyshiwodedidi'相同,所以個體的基因長度肯定是14。 綜上,個體應該是個長度為14的隨機小寫字母列表。

# 個體編碼
geneLength = 14#字串內有14個字元
toolbox = base.Toolbox()
toolbox.register('genASCII',random.randint, 97, 122)#英文小寫字符的ASCII碼範圍為97~122
toolbox.register('individual', tools.initRepeat, creator.Individual, toolbox.genASCII, n=geneLength)
#print(toolbox.individual())

小寫英文字母的ASCII碼範圍是97~122,而且ASCII碼肯定是整數,所以隨機函數應該是隨機生成整形(int),所以註冊函數寫成:

toolbox.register('genASCII',random.randint, 97, 122)

再將這個註冊函數代入到個體的註冊函數裡面即可。

toolbox.register('individual', tools.initRepeat, creator.Individual, toolbox.genASCII, n=geneLength)

評價函數設計

  我們想要評價一個個體和’zzyshiwodedidi‘的相似程度,自然要進行比較,字元顯然不方便直接比,但是轉換成ASCII碼(數字)顯然就方便的多。   於是評價函數的設計思路就是:將目標’zzyshiwodedidi'轉化為ASCII碼,然後與個體的基因依次對比(個體本身就是ASCII碼)。

def evaluate(ind):
    target = list('zzyshiwodedidi')#需要匹配的字串
    target = [ord(item) for item in target]#將字串內的字元都轉換成ASCII碼
    return (sum(np.abs(np.asarray(target) == np.asarray(ind)))),

使用ord函數將字串裡的字元依次轉換為ASCII碼。

	target = list('zzyshiwodedidi')#需要匹配的字串
    target = [ord(item) for item in target]#將字串內的字元都轉換成ASCII碼

依次轉換完成後需要依次比對,這裡需要注意一件事,將目標字串轉換為ASCII碼序列,與直接的隨機數列表個體的資料形式不同

在這裡插入圖片描述     

由上圖可見,第一行個體是列表格式,元素之間用,割開;而第二行目標字串轉換完後是矩陣形式,是一個行向量。   因此需要將個體使用np.asarry函數轉換為矩陣形式:

 return (sum(np.abs(np.asarray(target) == np.asarray(ind)))),

  當然只需要轉換個體就行了:

 return (sum(np.abs(target == np.asarray(ind)))),

精英選擇策略

精英選擇策略,即在每一輪迭代循環中,將父代與交叉變異後的子代整一塊,形成一個雙倍於之前規模的大種群,隨後在這個大種群中提取出一半適應度最優的個體。 說白了就是父代子代一起評,前一半的全提走,後一半的全踢走。 顯然,這樣的做法會加速收斂,快速得到一個充滿高適應度個體的精英種群。 這個策略以後也可以叫內卷策略

#精英選擇策略,加速收斂 
    combinedPop = pop + offspring#將子代與父代結合起來
  • 多方趨勢

    • 觀察大盤指數(道瓊指數/上市櫃指數)
    • 只做初升跟主升
    • 追趨勢盤整後第一天突破5% 做隔日沖 or 抱波段
  • 盤整

    • 做短線 (當沖/隔日沖)
      • 均線糾結第一天發動> 5% 尾盤進場
      • 篩選盤整股票隔天程式掛9.5% 進場
  • 向上加碼 (槓桿商品) or 美股 (趨勢太長幾年連續上漲)

  • ex: 臺指每賺200點加碼一次

    • 期貨拆10筆進場
    • 看對行情,等待回檔才向上加碼,並不是每格200點加碼這樣風險大,要等回檔再加碼通常都是技術分析上,一個很好的買點
      • 應該要賺300點後,回檔到200點再加碼
      • 後期脫離總成本遠安全就可以加大點 (需要計算)
  • 向下攤平 (臺股勝率高)

    • 用在止跌反轉 (崩跌線型 ?)
    • 進場點
      • 第二筆打進去需判斷
        • 離止跌點很近
        • 近情要起漲
        • 波動率邊緣進場 ATR 低波動?
    • 如何設定停損?
      • 需根據分析設定停損,所以停損要小 5~8%?
      • 分析的止跌點停損設很大是凹單
    • 保留一筆資金在股價高於成本區後再進場 ex: 40萬 30萬 拉過成均成本再進去 30萬(最後一筆)
      • 為什麼最後一筆要拉過成本再進怕遇到崩跌,全部錢會吐回去
  • 爆賺

    • 下時代會是誰? ex: 現在是特斯拉在火,誰讓特斯拉逼著起來
    • 找好商品(獨角獸)
    • 找好買點才可以持續加碼
      • 崩跌後出現
    • 轉機股、未來成長股
    • 成本低才好加碼
    • 張鬆允月K長紅發動後,等待拉回量縮持續買(能爆賺?)
  • 逆勢好處

    • 順勢交易進場點價位通常不好
    • 所以要順大逆小(逆KD)
  • JG

    • 逆KD /逆布林

      • 逆布林
        • 多頭趨勢使用 跌破下緣買進,並設好停損 通過通道上緣不要停利,向上分批賣出
    • 恐慌 定義

      • 多頭趨勢裡
      • 技術分析者恐慌
    • 賭博式停利法

      • 拿獲利一半來賭
    • YT 網友: 多頭市場很適合向下攤平

      • https://www.youtube.com/watch?v=5AQHkBoUybA

https://medium.com/blacksecurity/%E8%88%87%E7%9C%BE%E4%B8%8D%E5%90%8C%E7%9A%84%E6%80%9D%E8%80%83-%E5%8F%8D%E5%B8%82%E5%A0%B4-jg%E8%82%A1%E5%B8%82%E6%93%8D%E4%BD%9C%E5%8E%9F%E7%90%86-%E8%AE%80%E5%BE%8C%E5%BF%83%E5%BE%97-990c5cb13fc3

  • 股市是一個推理遊戲,想贏就得先搞懂「反市場」

    • 十次跌破裡面有八次是假跌破,是不是跌破去買進才是真道理?
    • 停損停利人人會設,你是機械式地提早賣掉飆漲股,還是有策略放大你的好運?
    • 許多人推崇KD指標、布林通道,但是否可以改良、做出不同選擇?
    • 避開主流,別把自己交給機率,盡可能的放大運氣才能暴賺。
  • 不合人性,任何方法都會賠錢

    • 存股很健康正向,但你知道六成優質股曾股價腰斬,許多人「存不住」而虧損出場嗎?
    • 看財報、看線圖……有各種預測股價的技術,但你想過「高勝率」不如「暴賺致富」嗎?
    • 操盤賺錢的關鍵不在預測,不在求穩定,而是要用符合人性的方式來長期操作。
    • 弄懂「反市場」心理,你才能在別人害怕的時機買進,在別人過於樂觀的時機賣出!
  • 用買賣來攻擊,用情緒來防守

    • 股市裡沒有一個人是技術超強但心理脆弱,卻還能成為贏家。
  • JG的八原則:

    • 股票市場就是賭
    • 務必和股市預言保持距離
    • 財報選股,離爆賺實在太遠
    • 爆賺,是最健康的股市態度
    • 當然要知道輸家的下一步
    • 優勢為輸贏之間的最大分水嶺
    • 贏家第一課:風險報酬比
    • 要賺一輩子,一定要有全面性的操盤力

CH1 反市場:股市致富之道

第一章部分提到運氣的重要性,JG強調運氣是股市重要的一個點,對於運氣的信仰決定你成為怎麼樣的投資者,運氣型玩家有個精神就是拿到好牌就要爆賺一筆。不過拿到好牌大多數人也不知道怎麼打這副好牌。

CH2 J派買賣原則

停損

停損並非代表自己看錯,而是讓自己把握其他賺錢機會

風險報酬比

JG有強調風險報酬比在投資前的心態很重要

風險報酬比為:

虧損:獲利  --> 建議為 1:3 

意思是賺一次可以抵賠錢三次,進場四次贏超過一次就可以

CH3 J派核心原理:反市場

技術指標

KD人人皆知但充滿不確定性,指標是不精確的結果而且不是原因,打開五分線看盤會發現一下交叉一下沒交叉,會有無數次的交叉。臺股和美股不同,絕對不能追高,因為波動有限。

KD指標

JG推薦逆KD,像是KD向下交叉+下跌買進搭配書中寫得停利與停損和其他條件。

股市預言

股市預言請全部打叉,市場的短期波動是被消息與躁動散戶所影響。

SUMMARY

用反市場避開主流才能爆賺,別把人生交給機率,拿到好牌就狠狠賭下去

CH4 不合人性,任何方法都會賠錢(存股與複利)

我看了不少有關存股、複利、價值投資或中長期投資的影片或書籍,這段大概會有很多爭議或不同觀點,也會有人提出解法或觀點,但一百種人就有一百種投資方法(與觀點),能賺錢就是好方法,而且我更好奇JG對於ETF看法就是。

中長期投資入門可參考另一篇心得:我的職業是股東金融名詞解釋

複利

市場流行一種觀念,低報酬=安全。

複利本身意義是用賺來的錢不斷在投入股市去利滾利,關鍵不是安全而是要持續不斷高報酬。

心理上大多人想要快速致富,盲目地投入就導致失敗,JG建議先求爆賺後求穩定,最後求不敗

存股

存股概念:隨時買+賭不會倒閉+高殖利率

前提是你在低點買,加上人性很容易導致失敗,市面上老師大多數都是從金融海嘯後才出來出書,又遇到大多頭市場所以全部都是對的。

盲點:忽略買點

假設以2009存到2019績效最好

假設以2006存到2016 績效稍微差一點

假設以2004存到2014 績效會非常差

心理上還是最大的敵人,靠配股配息會失敗是因為賺得少、賠得多,大金額的帳面虧損下跌顯違反人性,無法抱住。

優化存股:

  1. 買在最有利的景氣區間
  2. 買成長股,例如當時的玉山金,不是因為他很安全而是他會成長。
  3. 成長股要挑資本額20億以下比較好的成長動能
  4. 成長擺第一 ,股利股息擺第一
  5. 找好機會一次出手,除了巴菲特,年輕的投機客大多數都是基本面與基數面雙主修。

預測

這點倒是和經典書一樣的道理,股市不能預測。

CH5 脫離輸家的反市場思考

下單

下單沒有邏輯,用感覺下單,而不是推論。

趨勢底下,所有技術分析都是一坨屎

明牌

聽到的必死無疑 ,沒有更多資訊,只有一句話,若有資訊也不知道轉了幾手的消息。

好明牌一定要有未來性,還要追查來源證實理由

雜誌與市場

當市場的氣氛是恐慌時候,一定要當個冷靜的人,因為股票市場大多是賠錢的,而賠錢的人說話不需要聽,股市多頭時候誰講的話都是對的。

CH6 建立贏家的心理正回饋

贏家的心理與技術是兼具的。

股市癮

每天都想進出股市,一定要下單穩+準確的目標。

CH8 反市場贏家的八個原則

  1. 股票市場就是賭 — 不要以為自己可以掌控市場,很多情況都是運氣。不把股市當賭且不停損停利。容易患得失。
  2. 務必和股市預言保持距離 — 應思考是否為正確的真利多。
  3. 財報選股,離爆賺實在太遠 — 唯一要注意的是資本支出,代表董監持股信心。
  4. 爆賺,是最健康的股市態度 — 不要有短進短出的壞毛病
  5. 當然要知道輸家的下一步
  6. 優勢為輸贏之間的最大分水嶺
  7. 贏家第一課:風險報酬比
  8. 要賺一輩子,一定要有全面性的操盤力 — 總體經濟學搭配技術面比對

總體經濟弱+技術強=市場超強->重壓

總體經濟好+技術弱=市場超漲有短線疑慮->減碼

總體經濟好+技術強=安心持有

總體經濟差+技術弱=空手

崩潰線型

img

img

**正方形黃色=發動點第一根;**藍色圓圈是買點。

我建議要抓就抓兩根以上的大黑棒,每根超過1%(越多越好、越猛越好) 基本上已經可以用這個推論出很多更進階甚至更漂亮的買點。

這招在季線上揚(包括向上震盪)的時候非常關鍵好用 而且這種買點通常都是投資人最恐慌的時候,但知識就是力量,賺錢就是靠這個。

只是務必不要太求精準,太精準會錯過許多買點,瞭解其中的涵義更為重要 例如你可以想想,那空頭怎麼用,反著用就好。

而這些你如果願意再花點時間回測,我相信你還可以找到更多更好的潛在買點來提升報酬率,祝大家在股市順利!

img


1. 轉折
2. 右側交易 轉折處往上往下點數進場 守轉折處
3. 個股開盤試搓 漲跌停
4. 外資買賣超 前100名 漲跌停
5. 外資買超漲停 隔天拉高轉折往下做空 守轉折 反之亦然
6. 5大 10大法人
7. 一買一賣 往下買 不是攤平


進場策略: 3倍數口數
選股: 前天 外資買賣超 前100名 漲跌停
外資買超漲停 隔天拉高轉折往下做空 守轉折 反之亦然
例如: 高低點10點轉折進場
作空: 開165 漲到 180高點 後轉折往下到170 進場 or 跌破平盤
作多: 開165 跌到 150低點 後轉折往上到160 進場 or 漲破平盤

出場策略:(6R )
1R 2R(損益兩平) 3R(賣一口) 6R 再賣一口 剩餘一口去賭放到尾盤平倉 或是漲跌停第二次打開再平倉

ex:
現貨8:45 漲停, 但股票期貨沒有漲停那強代表有問題
9:00 開盤169往下空在168
RISK 設定 1
167(1R)
166(2R) 點到166後就反彈再點到168 就平倉損益兩平
165(3R)賣一口
162(6R)再賣一口
剩餘一口去賭放到尾盤平倉 或是漲跌停第二次打開再平倉

成交量該怎麼看?掌握量價關係2大原則抓對進出場點

https://winsmart.tw/online_teaching/%E6%88%90%E4%BA%A4%E9%87%8F/

  1. 透過成交量抓住進出場點位

  2. 再來是「風險控管」,我會建議大家可以將單筆損失控制在本金的2%以內

  3. 第三個條件是「方法」,利用今天說的短線操作方法「買在小量,出在大量。」 要如何買在小量其實有很多方法,像是你可以透過趨勢線做單,價格突破趨勢線進場做多,這種時候可能成交量還沒那麼大,漲一段以後行情拉出去爆大量,這個時候就會是你短線停利的地方,不要等到大量之後才去追價,應該是在行情還沒有大根K棒的時候就進場提早卡位。

大量漲不動:多單停利、空單進場
大量跌不動:空單停利、多單進場

資源

  • fmz

https://github.com/fmzquant/strategies/blob/master/README.md

Nansen

Nansen專注於追蹤鏈上活動,以"為鏈上地址打標簽"而出名,其中最有名的標簽就是“smart money”--聰明錢。擁有聰明錢標簽的是加密世界中的精英們的錢包地址,他們的交易行為通常能帶來不菲的收益,其他交易者自然也想要跟隨他們的交易行為。

Nansen擁有很強的快速迭代能力,比如在NFT市場興起之後,快速推出了一係列NFT相關的產品服務,如NFT Paradise、 NFT God Mode、NFT Wallet Profiler等。

【主要功能】

Nansen提供的功能主要有portfolio、smart alerts、watchlist等。在Nansen的主界麵上,可以查看主要公鏈的宏觀數據、defi數據、穩定幣數據和NFT市場的基本情況,包括主要NFT的市值、地板價、成交量、成交額和持有錢包數等。

鏈上數據分析工具盤點

portfolio:Nansen的用戶可以使用錢包地址登錄他們的網站,登錄後,就可以在頁麵上查看錢包地址下所有的資產、交易記錄、資產分析等。

鏈上數據分析工具盤點

smart alerts:Nansen允許用戶訂閱智能警報,在他們訂閱的地址進行活動時他們會收到通知。

watchlist:用戶可以向watchlist中添加想要監測的錢包地址,來隨時監測該地址的動向。

目前Nansen提供一部分免費功能,但絕大部分功能需要付費使用,這也是nansen的主要收入來源。

【是否支持自定義數據】

目前Nansen提供的數據都是經過他們處理的,不支持用戶自定義的數據分析。由於他們麵向的主要是需要高可用數據的機構投資者,因此提供的數據基本都是已經建模處理過後的成品數據。

【覆蓋區塊鏈】

Nansen已經支持了包括 Layer 2 在內的41條公鏈上的數據。

【數據延遲】

分鐘級彆延遲。

【研報】

Nansen擁有一個由18名分析師組成的分析團隊,在他們網站的nansen research板塊中為用戶提供研報,研究的內容涵蓋了L1/L2, NFT, GAMING, DEFI和宏觀趨勢等。

https://pro.nansen.ai/

交易所資金流動狀況 https://pro.nansen.ai/exchange-flows/exchanges

https://portfolio.nansen.ai/dashboard/binance https://portfolio.nansen.ai/dashboard/okx https://portfolio.nansen.ai/dashboard/bybit https://portfolio.nansen.ai/dashboard/bitfinix

通知 slack , tele

Smart Alerts

Nansen.ai - 如何追蹤大戶

Nansen:鏈上數據分析初步指南


幣安出現什麼徵兆時要塊陶?

作為一個hodler 熊市時我其實不太關注市場消息 這也導致此次FTX事件中後知後覺 被關廁所損失近40萬鎂

雖然主要資產是放在冷錢包中, 但這個損失還是不少 如果回到11/05, 是否有什麼確切證據可以知道FTX已經發生擠兌?

(板上的示警文章已看過, 有板友也因此得救, 但我想知道的是確實數據)

  1. 以前常用的例如 Whale Alert: https://twitter.com/whale_alert

    能否事先看出來? 在搜尋列輸入過濾條件 ftx from:whale_alert since:2022-11-05 從11/05到寫這篇文章的期間只有30筆alert, 坦白說看不出什麼跡象

    過濾條件: binance from:whale_alert since:2022-11-05 可以看到過去幾天有相當大額的#USDC burned (可能為提現)

    實際來說, 一家交易所如果要避開whale alert的偵測, 只要切細成較小額度 就能辦到, 例如100M USD切成1M x 100, 就偵測不到 所以也許FTX原本就是都切成小額居多, 而幣安比較沒這樣做 無論如何, 用whale alert似乎不太靠譜

  2. 使用鏈上數據分析, 例如Nansen: https://pro.nansen.ai/

    有七天試用期, 長期用要繳費, 另外它有twitter: https://twitter.com/nansen_ai

    正在示警各穩定幣和ETH大量流出:

    Exchange ETH & ERC20 tokens netflow in the last hour

    Binance -$72.9M Huobi -$12.7M Gateio -$7.3M Cryptocom -$4.7M OKX -$3.1M

    Bittrex +$771K Bitkub +$496K Paribu +$191K Bitpanda +$55K Probit +$51K

    *ETH & ERC20 tokens only, from addresses that we have labeled

Most of the withdrawals/negative netflows are in

USDT -$24.3M ETH -$11.7M USDC -$10.7M LDO -$6.8M BUSD -$6.5M

過去7天交易所流出排行: https://twitter.com/nansen_ai/status/1591617986275966976/photo/1

Jump Trading in the last 24 hours:

Total withdrawals from exchanges: $32.5M

Total deposits: $1.5M

*ETH & ERC20 tokens only, from addresses that we have labeled

In the last 7 days:

Total withdrawals: $691M

Total deposits: $229M

過去30天交易所流出排行: https://twitter.com/nansen_ai/status/1591617991648890880/photo/1

In the last 30 days:

Total withdrawals: $1.5B

Total deposits: $750M

不意外的看到FTX流出42B, 但只回補了39B, 而幣安則流出45B 此外鏈新聞在11/07示警FTX流出大量穩定幣也是採用nansen的數據: https://abmedia.io/20221107-ftx-stable-coin-outflow

用戶避風頭?FTX穩定幣發生大量提幣,幣安流入超過3億! | 鏈新聞 ABMedia [圖]資產機構 Alameda Research 的資產結構疑雲事件在近期引起不少討論,甚至引起 FTX 及幣安執行長的網路論戰、籌碼較勁。若從鏈上數據角度出發,可發現此事件已對 FTX 用戶造成影響並引發大量提幣,使數億美元的穩定幣從 FTX 流出。 ...

但這篇新聞的圖表並非來自twitter, 且它還列出各家交易所穩定幣餘額 並示警FTX在11/07時就只剩261M美金的穩定幣了

看來使用Nansen是可以看得出來, 前提是付費使用者以及有習慣看圖表

  1. 追蹤名人的推特: 除了以上提到的以外, CZ呀、kraken官方..等等的也許能得到警訊

  2. 查看交易所列出的錢包: 在這個恐慌期中, 各家交易所又想起了PoR的重要性, 但這也不是能短時間做好的, 以幣安為例, 就先給出了它的冷熱錢包: https://reurl.cc/LXMAmy

    其中BUSD和BNB先不用管, 專注看它的BTC, ETH, 和USDC錢包即可

    這三種錢包加總約有十幾B美金的價值, 但別忘了FTX光11/08單日就被提款50B 如果爆發恐慌擠兌, 以幣安市佔為FTX 4倍來估算, 這點錢應該是瞬間就空了

實際動作: 鑒於FTX的爆炸, 我開始轉出除了kraken以外交易所裡的資產 BTC和ETH直接打入冷錢包, 其餘小幣都賣成穩定幣 以幣安來說就是都賣成BUSD, 然後走erc20匯出成USDC

如果很多人也正在跟我做一樣的事, 則BTC和ETH相對抗跌 也不排除有人是賣小幣買成ETH存冷錢包的 而其他幣無可避免要先崩一波了

example

from plotly.offline import plot
import plotly.graph_objs as go
import pandas as pd
import talib as ta
import re 
import numpy as np

o = np.array([ 39.00, 39.00, 39.00, 39.00, 40.32, 40.51, 38.09, 35.00, 27.66, 30.80, 39.00, 39.00, 39.00, 39.00, 40.51, 38.09, 35.00, 27.66, 30.80])
h = np.array([ 40.84, 39.00, 40.84, 40.84, 41.69, 40.84, 38.12, 35.50, 31.74, 32.51, 40.84, 39.00, 40.84, 40.84, 40.84, 38.12, 35.50, 31.74, 32.51])
l = np.array([ 35.80, 35.80, 35.80, 35.80, 39.26, 36.73, 33.37, 30.03, 27.03, 28.31, 35.80, 35.80, 35.80, 35.80, 36.73, 33.37, 30.03, 27.03, 28.31])
c = np.array([ 40.29, 40.29, 40.29, 40.29, 40.46, 37.08, 33.37, 30.03, 31.46, 28.31, 40.29, 40.29, 40.29, 40.29, 37.08, 33.37, 30.03, 31.46, 28.31])

print('CDL3BLACKCROWS ', ta.CDL3BLACKCROWS(o, h, l, c))

trace = go.Candlestick( #x= pd.to_datetime(dfohlc.index.values),
            open=o,
            high=h,
            low=l,
            close=c)
data = [trace]

plot(data, filename='go_candle1.html')

import pandas as pd
import mpl_finance as mpf
import matplotlib.pyplot as plt
from FinMind.data import DataLoader


def draw_candle_stick(df, index):
    fig = plt.figure(figsize=(12, 8))
    ax = fig.add_subplot(1, 1, 1)
    ax.set_xticks(range(0, len(df.index)))
    ax.set_xticklabels(df[index])
    mpf.candlestick2_ochl(
        ax,
        df["open"],
        df["close"],
        df["max"],
        df["min"],
        width=0.7,
        colorup="r",
        colordown="g",
        alpha=1,
    )


dl = DataLoader()

stock_data = dl.taiwan_stock_daily(
    stock_id="TAIEX", start_date="2018-09-01", end_date="2022-08-31"
)
print(stock_data)

# 新增月份與星期欄位

stock_data["weekday"] = pd.to_datetime(stock_data["date"]).dt.weekday
stock_data["month"] = pd.to_datetime(stock_data["date"]).dt.month
stock_data = stock_data[stock_data["weekday"] < 5]
stock_data = stock_data.dropna()

# 把數據normalize到開盤價, 方便我們做比較

stock_data["max"] = stock_data["max"] / stock_data["open"]
stock_data["min"] = stock_data["min"] / stock_data["open"]
stock_data["close"] = stock_data["close"] / stock_data["open"]
stock_data["open"] = stock_data["open"] / stock_data["open"]


# 對星期做簡單平均

stock_data_gb_week = stock_data.groupby(["weekday"]).mean().reset_index()
weekday = ["星期一", "星期二", "星期三", "星期四", "星期五"]
stock_data_gb_week["weekday"] = stock_data_gb_week["weekday"].apply(
    lambda x: weekday[x]
)
draw_candle_stick(stock_data_gb_week, "weekday")

stock_data_gb_month = stock_data.groupby(["month"]).mean().reset_index()
month = [f"{i}月" for i in range(0, 13)]
stock_data_gb_month["month"] = stock_data_gb_month["month"].apply(lambda x: month[x])
draw_candle_stick(stock_data_gb_month, "month")

EMA 楊雲翔

import yfinance as yf
import pandas as pd

def ema(data, period, N=2):
    """
    計算 EMA(指數移動平均)指標
    :param data: 包含價格數據的 Pandas DataFrame
    :param period: EMA 的時間週期
    :return: 包含 EMA 指標數據的 Pandas Series
    """
    # 計算平滑因子
    alpha = N / (period + 1)

    # 計算首個 EMA 值
    ema = data['Close'].ewm(alpha=alpha, adjust=False).mean()

    # 計算後續 EMA 值
    for i in range(1, len(data)):
        ema[i] = alpha * data['Close'][i] + (1 - alpha) * ema[i - 1]

    return ema

# 獲取股票數據
symbol = 'AAPL'  # 股票代碼
start_date = '2021-01-01'  # 開始日期
end_date = '2021-12-31'  # 結束日期
data = yf.download(symbol, start=start_date, end=end_date)

# 計算 EMA 指標
data['N2'] = ema(data, 10)
data['N1'] = ema(data, 10, 1)

# 輸出結果
print(data)

統計大盤每年平均日K振幅

import yfinance as yf
import pandas as pd

# Fetch the TWSE index
twse = yf.Ticker("^TWII")

# Get the historical data for the longest possible time
history = twse.history(period="max")

# Calculate the daily range
history["Range"] = history["High"] - history["Low"]

# Group the data by year and calculate the average daily range
yearly_adr = history.groupby(pd.Grouper(freq="Y"))["Range"].mean()

# Print the yearly ADR
print(yearly_adr)

統計假突破次數

'''
統計假突破次數的方法因策略而異,以下提供一個假設進場條件的範例:

假設在 5 日均線上,且 5 日均線上穿 20 日均線時買進,其他時間賣出。
接下來我們將 K 線資料與以 5 日、20 日為週期的均線做比較,並建立一個叫做假突破的佈林通道,當價格穿越了假突破的通道時,就表示有一個假突破信號。 通道的計算方式為:
上通道:上一期假突破的最高價+1.5*(上一期假突破的最高價-上一期假突破的最低價)
下通道:上一期假突破的最低價-1.5*(上一期假突破的最高價-上一期假突破的最低價)
當發現股價穿越了假突破區間時,即可記錄一次假突破。
'''
import numpy as np
import pandas as pd
import yfinance as yf

# 下載開高低收成交量(ohlcv)資料
symbol = 'AAPL' # 蘋果公司
ohlcv = yf.download(symbol, start="2016-01-01", end="2021-11-17")

# 設定進場和出場訊號
ohlcv['ma5'] = ohlcv['Close'].rolling(window=5).mean()
ohlcv['ma20'] = ohlcv['Close'].rolling(window=20).mean()

ohlcv["in_signal"] = np.where(ohlcv['ma5'] > ohlcv['ma20'].shift(1), 1, 0)
ohlcv["out_signal"] = np.where(ohlcv['ma5'] <= ohlcv['ma20'].shift(1), 1, 0)

# 設定假突破的條件
ohlcv_in = ohlcv[ohlcv['in_signal'] == 1]
ohlcv_out = ohlcv[ohlcv['out_signal'] == 1]

fake_breakout_upper = np.nan
fake_breakout_lower = np.nan

fake_breakout_upper_list = []
fake_breakout_lower_list = []
in_signal_flag = False

for date, row in ohlcv.iterrows():
    if in_signal_flag:
        if row['High'] > fake_breakout_upper:
            fake_breakout_upper = row['High']
        if row['Low'] < fake_breakout_lower:
            fake_breakout_lower = row['Low']
        
        if row['Low'] <= fake_breakout_lower and row['Close'] > fake_breakout_lower and not np.isnan(fake_breakout_lower):
            fake_breakout_upper_list.append(fake_breakout_upper)
            fake_breakout_lower_list.append(fake_breakout_lower)
            
            fake_breakout_upper = np.nan
            fake_breakout_lower = np.nan
            
            in_signal_flag = False
            
    else:
        if date in ohlcv_in.index:
            fake_breakout_upper = row['High']
            fake_breakout_lower = row['Low']
            in_signal_flag = True
            
counts = len(fake_breakout_upper_list)

print(f"假突破次數: {counts}")

How to implement a Grid Trading Strategy (Python Tutorial)

https://medium.com/@chris_42047/how-to-implement-a-grid-trading-strategy-python-tutorial-338b38fc5e84

from pandas.tseries.holiday import USFederalHolidayCalendar
from pandas.tseries.offsets import CustomBusinessDay

US_BUSINESS_DAY = CustomBusinessDay(calendar=USFederalHolidayCalendar())
from pandas.tseries.holiday import USFederalHolidayCalendar
from pandas.tseries.offsets import CustomBusinessDay

US_BUSINESS_DAY = CustomBusinessDay(calendar=USFederalHolidayCalendar())
import pandas as pd
from backtesting import Strategy
from backtesting import Backtest
import pandas_ta as ta
import yfinance as yf
import plotly.graph_objects as go
from plotly.subplots import make_subplots


def CHOP(df, chop_len, atr_len):
    #  Calculate Choppiness
    chop_series = ta.chop(
        high=df["High"],
        low=df["Low"],
        close=df["Close"],
        length=chop_len,
        atr_length=atr_len,
    )
    return chop_series


def plot_chart(
    i, symbol, df, current_price, buy_grid, sell_grid, buy_stop_loss, sell_stop_loss
):
    light_palette = {}
    light_palette["bg_color"] = "#ffffff"
    light_palette["plot_bg_color"] = "#ffffff"
    light_palette["grid_color"] = "#e6e6e6"
    light_palette["text_color"] = "#2e2e2e"
    light_palette["dark_candle"] = "black"
    light_palette["light_candle"] = "steelblue"
    light_palette["volume_color"] = "#c74e96"
    light_palette["border_color"] = "#2e2e2e"
    light_palette["color_1"] = "#5c285b"
    light_palette["color_2"] = "#802c62"
    light_palette["color_3"] = "#a33262"
    light_palette["color_4"] = "#c43d5c"
    light_palette["color_5"] = "#de4f51"
    light_palette["color_6"] = "#f26841"
    light_palette["color_7"] = "#fd862b"
    light_palette["color_8"] = "#ffa600"
    light_palette["color_9"] = "#3366d6"
    palette = light_palette
    #  Array of colors for support/resistance lines
    buy_grid_colors = ["#e28743", "#e28743", "#e28743", "#e28743", "#e28743"]
    sell_grid_colors = ["#2596be", "#2596be", "#2596be", "#2596be", "#2596be"]
    #  Create sub plots
    fig = make_subplots(
        rows=1,
        cols=1,
        subplot_titles=[f"{i} {symbol} Chart",],
        specs=[[{"secondary_y": False}]],
        vertical_spacing=0.04,
        shared_xaxes=True,
    )
    #  Plot close price
    fig.add_trace(
        go.Scatter(
            x=df.index, y=df["Close"], line=dict(color="blue", width=1), name=f"Close"
        ),
        row=1,
        col=1,
    )
    #  Current price
    fig.add_hline(
        y=current_price,
        line_width=0.6,
        line_dash="solid",
        line_color="blue",
        row=1,
        col=1,
    )
    #  Add buy and sell grids
    i = 0
    for level in buy_grid:
        line_color = (
            buy_grid_colors[i] if i < len(buy_grid_colors) else buy_grid_colors[0]
        )
        fig.add_hline(
            y=level,
            line_width=0.6,
            line_dash="dash",
            line_color=line_color,
            row=1,
            col=1,
        )
        i += 1
    #  stop loss
    fig.add_hline(
        y=buy_stop_loss,
        line_width=0.6,
        line_dash="solid",
        line_color="red",
        row=1,
        col=1,
    )
    i = 0
    for level in sell_grid:
        line_color = (
            sell_grid_colors[i] if i < len(sell_grid_colors) else sell_grid_colors[0]
        )
        fig.add_hline(
            y=level, line_width=1, line_dash="dash", line_color=line_color, row=1, col=1
        )
        i += 1
    #  stop loss
    fig.add_hline(
        y=sell_stop_loss,
        line_width=1,
        line_dash="solid",
        line_color="red",
        row=1,
        col=1,
    )
    fig.update_layout(
        title={"text": "", "x": 0.5},
        font=dict(family="Verdana", size=12, color=palette["text_color"]),
        autosize=True,
        width=1280,
        height=720,
        xaxis={"rangeslider": {"visible": False}},
        plot_bgcolor=palette["plot_bg_color"],
        paper_bgcolor=palette["bg_color"],
    )
    fig.update_yaxes(visible=False, secondary_y=True)
    #  Change grid color
    fig.update_xaxes(
        showline=True,
        linewidth=1,
        linecolor=palette["grid_color"],
        gridcolor=palette["grid_color"],
    )
    fig.update_yaxes(
        showline=True,
        linewidth=1,
        linecolor=palette["grid_color"],
        gridcolor=palette["grid_color"],
    )
    file_name = f"{i}_{symbol}_grid_trading_1.png"
    fig.write_image(file_name, format="png")
    return fig


class GridStrategy(Strategy):
    chop_len = 14
    atr_len = 1
    num_grid_lines = 5  #  number of grid lines for buy/sell
    grid_interval = 10 / 10000  # 10 pips, 50 pips, or 100 pips or whatever
    take_profit_interval = 20 / 10000  #  pips
    stop_loss_interval = 10 / 10000  # pips
    buy_grid_prices = []
    sell_grid_prices = []
    executed_buy_grid_prices = []
    executed_sell_grid_prices = []
    last_purchase_price = 0
    long_hold = 0
    short_hold = 0
    buy_stop_loss_price = 0
    sell_stop_loss_price = 0
    grid_in_progress = False
    grid_start_index = 0  #  time index when grid starts
    grid_max_interval = 2000  #  max time steps to run the grid
    i = 0

    def init(self):
        super().init()
        #  Calculate indicators
        self.chop = self.I(CHOP, self.data.df, self.chop_len, self.atr_len)

    def reset_grid(self):
        self.grid_in_progress = False
        self.buy_grid_prices = []
        self.sell_grid_prices = []
        self.grid_start_index = 0
        self.buy_stop_loss_price = 0
        self.sell_stop_loss_price = 0

    def next(self):
        super().init()
        self.i += 1
        #  Check ranging or trending markets
        is_ranging = False
        if self.chop[-1] > 50 and self.chop[-2] <= 50:
            is_ranging = True
        #  Set up new grid for ranging -> against the trend
        current_price = self.data.Close[-1]
        if not self.grid_in_progress and is_ranging:
            self.reset_grid()
            self.grid_in_progress = True
            self.grid_start_index = self.i
            #  Stop loss
            buy_stop_loss = (
                current_price
                - (self.num_grid_lines * self.grid_interval)
                - self.stop_loss_interval
            )
            sell_stop_loss = (
                current_price
                + (self.num_grid_lines * self.grid_interval)
                + self.stop_loss_interval
            )
            #  Set buy/sell grid prices
            for i in range(1, self.num_grid_lines + 1):
                #  Calculate buy grid price
                grid_buy_price = current_price - (i * self.grid_interval)
                buy_take_profit = grid_buy_price + self.take_profit_interval
                self.buy_grid_prices.append(grid_buy_price)
                #  Create buy order
                self.buy(
                    size=0.1, limit=grid_buy_price, sl=buy_stop_loss, tp=buy_take_profit
                )
                #  Calculate sell grid price
                grid_sell_price = current_price + (i * self.grid_interval)
                sell_take_profit = grid_sell_price - self.take_profit_interval
                self.sell_grid_prices.append(grid_sell_price)
                #  Create sell order
                self.sell(
                    size=0.1,
                    limit=grid_sell_price,
                    sl=sell_stop_loss,
                    tp=sell_take_profit,
                )
            #  Optional - Plot the grid
            # plot_chart(self.i, symbol, df, current_price, self.buy_grid_prices, self.sell_grid_prices, buy_stop_loss, sell_stop_loss)


def run_backtest(df):
    # If exclusive orders (each new order auto-closes previous orders/position),
    # cancel all non-contingent orders and close all open trades beforehand
    bt = Backtest(
        df,
        GridStrategy,
        cash=10000,
        commission=0.00075,
        trade_on_close=True,
        exclusive_orders=False,
        hedging=False,
    )
    stats = bt.run()
    print(stats)
    bt.plot()


# MAIN
if __name__ == "__main__":
    symbol = "EURUSD=X"
    #  Download data
    # intervals: 1m,2m,5m,15m,30m,60m,90m,1h,1d,5d,1wk,1mo,3mo
    interval = "1m"
    #  periods:  1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max
    data = yf.download(tickers=symbol, period="5d", interval=interval)
    df = pd.DataFrame(data)
    df.dropna(inplace=True)
    df.reset_index(inplace=True)
    #  Run backtest
    run_backtest(df)

TD Ameritrade API

import requests
import datetime
import pandas as pd

apikey = ""
url = "https://api.tdameritrade.com/v1/marketdata/hours"
payload = {"apikey": apikey, "markets": "EQUITY"}
response = requests.get(url=url, params=payload)
data = response.json()
print(data)

網格買/賣單數量計算

  • base_diff_ratio :

    • 此值動態計算得到, 如果大於1則設定為1
    • 網格預估需要的base數量與實際外站能買到base數量比例
    • ex: 網格預估5顆 但實際外站買到4.5顆, - base_diff_ratio = (4.5 / 5) = 0.9
  • 各價位點計算

def round_to_rule(value: float, rule: str) -> float:
    return round(value, int(rule))


def round_to_rule_floor(n, decimals=0):
    multiplier = 10 ** int(decimals)
    return math.floor(n * multiplier) / multiplier

# 網格間距
grid_step = round_to_rule_floor((upper_limit - lower_limit) / float(grid_count), quotePrecision) 

price_points = []
price_points.append(upper_limit)
while True:
    price_point = round_to_rule(price_points[-1] - grid_step, quotePrecision)

    if upper_limit >= price_point and price_point >= lower_limit:
        price_points.append(price_point)

    if price_point <= lower_limit:
        print(f"各價位點:{price_points}")
        break

全買單 (現價 > 上限)

註: 全買單沒有站外買幣需要因此 base_diff_ratio 固定為1

current_price = 658990 
capital = 1000 
grid_count = 2 
upper_limit = 4 
lower_limit = 1 
base_diff_ratio = 1
# capital = capital * base_diff_ratio * 0.998;  
1000 * 1 * 0.998 = 998

# grid_position = capital / (SUM(各價位點, 不包含最下面那格)  
round(998 / (4+3+2), 8) = 110.88888888888889

# fee = (grid_position * 最高價 * 0.002 * grid_count) + (1 * grid_count) 
(110.88888888888889 * 4 * 0.002 * 2) + (1 * 2) = 3.774222222222222

# capital = capital - fee  
(998 - 3.774222222222222) = 994.2257777777778

# grid_position = capital / (SUM(各價位點, 不包含最下面那格)  
round(994.2257777777778 / (4+3+2), 8) = 110.46953086

全賣單 現價 < 下限

註: 全賣單會先預扣買單所需要手續費

current_price = 658990 
capital = 1000 
grid_count = 2 
upper_limit = 800000 
lower_limit = 700000 
base_diff_ratio = 1.0894263392162356
# grid_position = capital / (SUM(各價位點, 不包含最下面那格) 
round(1000 / (800000 + 750000), 8) = 0.00064516

# fee = (grid_position * 最高價 * 0.002 * grid_count) + (1 * grid_count)  
(0.00064516 * 800000 * 0.002 * 2) + (1 * 2) = 4.064512000000001

# capital = (capital - fee) * 1 * 0.998;    # base_diff_ratio 大於1則設定為1
(1000 - 4.064512000000001) * 1 * 0.998 = 993.943617024

# grid_position = capital / (SUM(各價位點, 不包含最下面那格) 
round(993.943617024 / (800000 + 750000), 8) = 0.00064125

部份買單部份賣單 (上限> 現價 >下限)

current_price = 658990 
capital = 1000 
grid_count = 2 
upper_limit = 800000 
lower_limit = 600000 
base_diff_ratio = 0.86
# capital = capital * base_diff_ratio * 0.998
(1000 * 0.86 * 0.998) = 858.28

# grid_position = capital / (SUM(各價位點, 不包含最下面那格)  
round(858.28 / (800000 + 700000), 8) = 0.00057219

# fee = (grid_position * 最高價 * 0.002 * grid_count) + (1 * grid_count) 
(0.00057219 * 800000 * 0.002 * 2) + (1 * 2) = 3.8310079999999997

# capital = capital - fee  
(858.28 - 3.8310079999999997) = 854.448992

# grid_position = capital / (SUM(各價位點, 不包含最下面那格)
round(854.448992 / (800000 + 700000), 8) = 0.00056963

網格顆數計算

import math


def round_to_rule(value: float, rule: str) -> float:
    return round(value, int(rule))


def round_to_rule_floor(n, decimals=0):
    multiplier = 10 ** int(decimals)
    return math.floor(n * multiplier) / multiplier


def get_fee(upper_limit, lower_limit, grid_count, basePrecision, quotePrecision):
    price_points = []
    grid_step = round_to_rule_floor(
        (upper_limit - lower_limit) / float(grid_count), quotePrecision
    )
    print(f"grid step:{grid_step}")
    price_points.append(upper_limit)
    while True:
        price_point = round_to_rule(price_points[-1] - grid_step, quotePrecision)

        if upper_limit >= price_point and price_point >= lower_limit:
            price_points.append(price_point)

        if price_point <= lower_limit:
            print(f"各價位點:{price_points}")
            break

    # round(998 / (200000+ 160080.0+ 120160.0+ 80240.0+ 40320.0), 8) = 0.00166112
    grid_position = round_to_rule(capital / sum(price_points[:-1]), basePrecision)
    # print(grid_position)
    return (
        (grid_position * price_points[0] * 0.002 * grid_count) + (1 * grid_count),
        price_points,
    )


def get_grid_position(
    capital, upper_limit, lower_limit, grid_count, basePrecision, quotePrecision
):
    fee, price_points = get_fee(
        upper_limit, lower_limit, grid_count, basePrecision, quotePrecision
    )
    # 資金扣除預留手續費
    # 998 - ((0.0016611185086551265 * 200000 * 0.002 * 5) + (1+5)) = 988.6777629826897
    if current_price < lower_limit:
        fee = 0

    print(f"get_grid_position fee:{fee}")
    capital = capital - fee
    print(f"扣除手續費後的資金:{capital}")

    # 扣掉預留費用,每格該下的btc數量,之後下單以這個為準
    grid_position = round_to_rule(capital / sum(price_points[:-1]), basePrecision)
    return grid_position


if __name__ == "__main__":
    current_price = # Test Log - base price bitopro
    capital = 1000
    grid_count = 2
    upper_limit = 800000
    lower_limit = 600000
    base_diff_ratio = # Test Log - base difference ratio
    basePrecision = 8
    quotePrecision = 0
    fee = 0

    # 目前價格小於網格最低價
    if current_price < lower_limit:
        fee, _ = get_fee(
            upper_limit, lower_limit, grid_count, basePrecision, quotePrecision
        )

    if base_diff_ratio > 1.0:
        capital = (capital - fee) * 0.998
    else:
        capital = (capital - fee) * base_diff_ratio * 0.998

    print(f"capital:{capital}, fee:{fee}")
    grid_position = get_grid_position(
        capital, upper_limit, lower_limit, grid_count, basePrecision, quotePrecision
    )
    print(f"grid position:{grid_position}")

DeFiLlama

from defillama import DefiLlama
import json

import requests

# initialize api client
llama = DefiLlama()

# Get all protocols data
response = llama.get_all_protocols()
#print(response, type(response[0]))
print(json.dumps(response[0], indent=4, ensure_ascii=False))

# Get a protocol data
response = llama.get_protocol(name='uniswap')
print(json.dumps(response, indent=4, ensure_ascii=False))

# Get historical values of total TVL
response = llama.get_historical_tvl()
print(json.dumps(response[0], indent=4, ensure_ascii=False))

# Get protocol TVL
response = llama.get_protocol_tvl(name='uniswap')
print(json.dumps(response, indent=4, ensure_ascii=False))

MultiCharts用「斜率」/「角度(LinearRegAngle)」定義趨勢變化

https://www.pfcf.com.tw/featured/detail/2010

線性回歸

首先讀者要稍微回憶一下中學數學--線性回歸(linear regression),可自行GOOGLE相關資料,不想花時間知其所以然,也沒關係,直接往下讀也不難上手應用。

透過線性回歸計算,我們可以把某個區間的價格(例如過去5根K棒收盤價)計算其線性關係,如下圖所示,θ即是這條線與Y軸的角度,m斜率即是度量y與x變動比率。無論角度或斜率,在Powerlanguage都有內建函式可供直接引用計算。

img

角度(LinearRegAngle)

行情強勢多頭,漲勢就會越接近90度,反之,行情重挫,下跌的角度就會接近-90度,很多人以為是要用斜率來表達價格趨勢的變化,其實用角度更為直觀。我們用LinearRegAngle這個函式可計算出角度:

LinearRegAngle(Price, Len)

例如我們想表達上漲強度變強,也就是角度越來越陡,PowerLanguage我們可以這樣寫:

Value1= LinearRegAngle(close, 5); Condition1=Value1>Value1[1];

意即,我們計算出最近5根K棒收盤價的線性角度,然後比較前一個角度,變大就代表角度變陡。因為單算收盤價的線性回歸往往變動太大反而難以定向趨勢,可以用均線來平滑取而代之,如下:

Value1=Average(close,5); Value2= LinearRegAngle(Value1, 5); Condition1=Value2>Value2[1] and Value2>60;

如上,我們可以再多指定一個閥值,例如60度,角度上升且大於60度,定義出行情為強勢多頭。也有人再深入,就進到微積分的領域了。

斜率(LinearRegSlope)

如前面所言,我們想表達(趨勢)斜率變陡或變平,我們其實想要的是角度而非斜率,或者說採用角度(LinearRegAngle)是比較值觀好用的,當然如果你堅持要用斜率也無不可,用法與上面角度一樣,斜率函式為:

LinearRegSlope(Price, Len)

結論與後記

其實我們都知道市場價格非線性,橫豎也回歸不出來,卻使用從線性回歸得出的角度或斜率似乎有些矛盾弔詭,不過無妨,這樣的運用提供我們一個量化且具體的數字跟邏輯,仍有其價值。在MultiCharts裡尚有Linear Reg Curve這個指標,主要是用到LinearRegValue這個函式,可以根據線性回歸公式算出目前甚至未來的”理論”價格,這類預測價格的做法,實務價值反而就不大了。


#!/usr/bin/env python
# coding: utf-8

# # Algo Trad Pipeline
# > A basic example of a algo trading pipeline with data fetch, strategy, backtest and online trading.
# 
# - toc: true 
# - badges: true
# - comments: true
# - categories: [jupyter]
# 

# ### Install dependence & init setting

# In[ ]:


pip install cryptota -U


# In[ ]:


# INIT
# Fetch data setting
CRYPTO    = "ADAUSDT"
START     = '7 day ago UTC'
END       = 'now UTC'
INTERVAL  = '1m'
# trading strategy parameter
PARAMETER = { "initial_state": 1, "delay": 500, "initial_money": 100,"max_buy":10, "max_sell":10 }
# binance api key and secret
APIKEY    = ""
APISECRET = ""


# In[ ]:


import cryptota
import vectorbt as vbt
import numpy as np
from binance import Client, ThreadedWebsocketManager, ThreadedDepthCacheManager
import matplotlib.pyplot as plt
import time
from datetime import timedelta

client = Client(APIKEY,APISECRET)


# In[ ]:


UNITS = {"s":"seconds", "m":"minutes", "h":"hours", "d":"days", "w":"weeks"}

def convert_to_seconds(s):
    count = int(s[:-1])
    unit = UNITS[ s[-1] ]
    td = timedelta(**{unit: count})
    return td.seconds + 60 * 60 * 24 * td.days


# ### Fetch data

# In[ ]:


binance_data = vbt.BinanceData.download(
    CRYPTO,
    start=START,
    end=END,
    interval=INTERVAL
)


# In[ ]:


price = binance_data.get()


# In[ ]:


price


# ### Get technology analysis feature

# In[ ]:


ta = cryptota.TA_Features()
df_full = ta.get_all_indicators(price.copy())


# In[ ]:


df_full


# ### Purpose a strategy

# In[ ]:


def buy_stock(
    real_movement,
    delay = 5,
    initial_state = 1,
    initial_money = 10000,
    max_buy = 1,
    max_sell = 1,
    print_log=True
):
    """
    real_movement = actual movement in the real world
    delay = how much interval you want to delay to change our decision from buy to sell, vice versa
    initial_state = 1 is buy, 0 is sell
    initial_money = 1000, ignore what kind of currency
    max_buy = max quantity for share to buy
    max_sell = max quantity for share to sell
    """
    starting_money = initial_money
    delay_change_decision = delay
    current_decision = 0
    state = initial_state
    current_val = real_movement[0]
    states_sell = []
    states_buy = []
    states_entry = []
    states_exit = []
    current_inventory = 0

    def buy(i, initial_money, current_inventory):
        shares = initial_money // real_movement[i]
        if shares < 1:
            if print_log:
                print(
                    'day %d: total balances %f, not enough money to buy a unit price %f'
                    % (i, initial_money, real_movement[i])
                )
        else:
            if shares > max_buy:
                buy_units = max_buy
            else:
                buy_units = shares
            initial_money -= buy_units * real_movement[i]
            current_inventory += buy_units
            if print_log:
                print(
                    'day %d: buy %d units at price %f, total balance %f'
                    % (i, buy_units, buy_units * real_movement[i], initial_money)
                )
            states_buy.append(0)
        return initial_money, current_inventory

    if state == 1:
        initial_money, current_inventory = buy(
            0, initial_money, current_inventory
        )

    for i in range(0, real_movement.shape[0], 1):
        sentry = False
        sexit = False
        if real_movement[i] < current_val and state == 0:
            if current_decision < delay_change_decision:
                current_decision += 1
            else:
                state = 1
                initial_money, current_inventory = buy(
                    i, initial_money, current_inventory
                )
                current_decision = 0
                states_buy.append(i)
                sentry = True
                
        if real_movement[i] > current_val and state == 1:
            if current_decision < delay_change_decision:
                current_decision += 1
            else:
                state = 0

                if current_inventory == 0:
                    if print_log:
                        print('day %d: cannot sell anything, inventory 0' % (i))
                else:
                    if current_inventory > max_sell:
                        sell_units = max_sell
                    else:
                        sell_units = current_inventory
                    current_inventory -= sell_units
                    total_sell = sell_units * real_movement[i]
                    initial_money += total_sell
                    try:
                        invest = (
                            (real_movement[i] - real_movement[states_buy[-1]])
                            / real_movement[states_buy[-1]]
                        ) * 100
                    except:
                        invest = 0
                    if print_log:
                        print(
                            'day %d, sell %d units at price %f, investment %f %%, total balance %f,'
                            % (i, sell_units, total_sell, invest, initial_money)
                        )

                current_decision = 0
                states_sell.append(i)
                sexit = True
        states_entry.append(sentry)
        states_exit.append(sexit)
        current_val = real_movement[i]
        
    invest = ((initial_money - starting_money) / starting_money) * 100
    total_gains = initial_money - starting_money
    return states_buy, states_sell,states_entry,states_exit, total_gains, invest


# ### Backtest

# In[ ]:


states_buy, states_sell, states_entry, states_exit, total_gains, invest = buy_stock(df_full.close,**PARAMETER)


# In[ ]:


close = df_full['close']
fig = plt.figure(figsize = (15,5))
plt.plot(close, color='r', lw=2.)
plt.plot(close, '^', markersize=10, color='m', label = 'buying signal', markevery = states_buy)
plt.plot(close, 'v', markersize=10, color='k', label = 'selling signal', markevery = states_sell)
plt.legend()
plt.show()


# In[ ]:


fees = 0.001
try:
  fees = client.get_trade_fee(symbol=CRYPTO)[0]['makerCommission']
except:
  pass


# In[ ]:


portfolio_kwargs = dict(size=np.inf, fees=float(fees), freq=INTERVAL)
portfolio = vbt.Portfolio.from_signals(df_full['close'], states_entry, states_exit, **portfolio_kwargs)


# In[ ]:


portfolio.plot().show()


# In[ ]:


portfolio.stats()


# ### Online

# In[ ]:


info = client.get_symbol_info(CRYPTO)
info


# In[ ]:


while True: 
    binance_data = binance_data.update()
    price = binance_data.get()

    states_buy, states_sell, states_entry, states_exit, total_gains, invest = buy_stock(price.Close, 
                                                                                        initial_state = 1, 
                                                                                        delay = 10, 
                                                                                        initial_money = 1,
                                                                                        max_buy=1,
                                                                                        max_sell=1,
                                                                                        print_log=False)

    states_entry[-1],states_exit[-1]
    
    if not (states_entry[-1] or states_exit[-1]):
        print("doing_noting")
    if states_entry[-1]:
        order = client.create_test_order( ## use test_order for real~
        symbol='ADAUSDT',
        side=Client.SIDE_BUY,
        type=Client.ORDER_TYPE_MARKET,
        quantity=8)
        print("buy",order)
    if states_exit[-1]:
        order = client.create_test_order( ## use test_order for real~
        symbol='ADAUSDT',
        side=Client.SIDE_BUY,
        type=Client.ORDER_TYPE_MARKET,
        quantity=8)
        print("sell",order)
    
    time.sleep(convert_to_seconds(INTERVAL))

import shioaji as sj
import json
from shioaji import constant


def order_cb(stat, msg):
    if "status" in msg:
        print(f"status: {json.dumps(msg['status'], indent=4)}")
        print(f"order: {json.dumps(msg['order'], indent=4)}")
        print(f"contract: {json.dumps(msg['contract'], indent=4)}")


def get_orders(api, account):
    api.update_status(account)
    trades = {t.status.id: t for t in api.list_trades()}

    for trade in trades.values():
        print(
            "Order ID: {}\nAction: {}\nStatus: {}\nStatus Value: {}\nOrder Condition: {}".format(
                trade.status.id,
                trade.order.action,
                trade.status.status,
                trade.status.status.value,
                trade.order.order_cond.value,
            )
        )
        print("-" * 30)


def cancel_all_orders(api):
    api.update_status()
    for trade in api.list_trades():
        if trade.status.status in [
            constant.Status.PreSubmitted,
            constant.Status.Submitted,
            constant.Status.PartFilled,
        ]:
            api.cancel_order(trade, timeout=0)


def test_stock(api):
    stock_contract = api.Contracts.Stocks.TSE["2890"]
    print(
        "Limit Up: {}\nLimit Down: {}".format(
            stock_contract.limit_up, stock_contract.limit_down
        )
    )

    stock_order = api.Order(
        price=23.15,
        quantity=1,
        action=constant.Action.Buy,
        price_type=constant.StockPriceType.LMT,
        order_type=constant.OrderType.ROD,
        account=api.stock_account,
    )
    stock_trade = api.place_order(stock_contract, stock_order)
    get_orders(api, api.stock_account)


def test_future(api):
    # 下單
    futures_contract = api.Contracts.Futures.TXF.TXFR1
    print(
        "Limit Up: {}\nLimit Down: {}".format(
            futures_contract.limit_up, futures_contract.limit_down
        )
    )

    futures_order = api.Order(
        action=constant.Action.Buy,
        price=21000,
        quantity=3,
        price_type=constant.FuturesPriceType.LMT,
        order_type=constant.OrderType.ROD,
        octype=constant.FuturesOCType.Auto,
        account=api.futopt_account,
    )
    futures_trade = api.place_order(futures_contract, futures_order)
    # print(f"\nFutures Trade: {futures_trade}")
    # get_orders(api, api.futopt_account)


def main():
    api = sj.Shioaji(simulation=True)
    api.login(
        api_key="",
        secret_key="",
    )

    # 設定委託回報函式
    api.set_order_callback(order_cb)

    # test_stock(api)
    test_future(api)

    # cancel_all_orders(api)
    # input("Press Enter to exit")
    api.logout()


if __name__ == "__main__":
    main()

Python的gRPC

Python的gRPC可以幫助你將不同的參數映射到不同的策略類別。對於介面包含初始化參數的問題,你可以考慮使用類別的繼承機制。以下是一個簡單的範例:

class BaseStrategy:
    def __init__(self, grid_num, upper_bound, lower_bound, funds):
        self.grid_num = grid_num
        self.upper_bound = upper_bound
        self.lower_bound = lower_bound
        self.funds = funds

    def generate_grid_batch(self):
        raise NotImplementedError

    def max_profit(self):
        raise NotImplementedError

    def min_profit(self):
        raise NotImplementedError

class GridStrategy(BaseStrategy):
    def __init__(self, grid_num, upper_bound, lower_bound, funds):
        super().__init__(grid_num, upper_bound, lower_bound, funds)

    def generate_grid_batch(self):
        # 生成網格批次單
        pass

    def max_profit(self):
        # 最大利潤
        pass

    def min_profit(self):
        # 最小利潤
        pass

class AnotherStrategy(BaseStrategy):
    def __init__(self, param1, param2, funds):
        self.param1 = param1
        self.param2 = param2
        super().__init__(None, None, None, funds)

    def generate_grid_batch(self):
        # 生成網格批次單
        pass

    def max_profit(self):
        # 最大利潤
        pass

    def min_profit(self):
        # 最小利潤
        pass

在這個範例中,我們定義了一個BaseStrategy基礎類別,包含一些共同的屬性和函數,並且我們明確表示generate_grid_batch()max_profit()min_profit()需要被子類別覆寫。

接下來,我們定義了GridStrategyAnotherStrategy類別,這兩個類別都繼承自BaseStrategy類別。GridStrategy類別的初始化函數接受網格數、上界、下界和資金參數,而AnotherStrategy類別的初始化函數接受兩個自定義的參數param1param2,並且也需要提供資金參數。

透過這種方式,你可以定義任意數量的策略類別,並根據不同的情況傳遞相應的參數。當你需要使用某個策略時,只需要實例化對應的類別即可。例如:

strategy1 = GridStrategy(10, 100, 50, 1000)
strategy2 = AnotherStrategy(0.5, 0.1, 2000)

在實例化之後,你可以根據需要調用每個策略對應的方法:

strategy1.generate_grid_batch()
strategy1.max_profit()
strategy1.min_profit()

strategy2.generate_grid_batch()
strategy2.max_profit()
strategy2.min_profit()

當你需要新增其他的策略時,只需要繼承BaseStrategy類別,並且實現相應的方法即可。這種方式不僅讓你的程式碼更易於擴展,同時也讓你的程式碼更加可讀和可維護。

當使用gRPC時,你需要定義服務和消息類別,並且實現相應的方法。在這個範例中,我們定義了一個StrategyService服務,其中包含一個GetStrategy方法,用於返回一個指定策略的實例。在客戶端,我們可以通過gRPC通道將參數傳遞給服務端,並獲取相應的策略實例。

以下是一個簡單的範例程式碼:

# strategy_service.proto
syntax = "proto3";

package strategy;

message StrategyRequest {
  string strategy_name = 1;
  int32 grid_num = 2;
  int32 upper_bound = 3;
  int32 lower_bound = 4;
  int32 funds = 5;
}

message StrategyResponse {
  // 將策略序列化後傳回客戶端
  bytes strategy = 1;
}

service StrategyService {
  rpc GetStrategy (StrategyRequest) returns (StrategyResponse) {}
}
# strategy_service.py
import grpc
from concurrent import futures
import strategy_service_pb2
import strategy_service_pb2_grpc
from strategy import GridStrategy, AnotherStrategy

class StrategyServicer(strategy_service_pb2_grpc.StrategyServiceServicer):
    def GetStrategy(self, request, context):
        strategy_name = request.strategy_name
        grid_num = request.grid_num
        upper_bound = request.upper_bound
        lower_bound = request.lower_bound
        funds = request.funds

        if strategy_name == 'GridStrategy':
            strategy = GridStrategy(grid_num, upper_bound, lower_bound, funds)
        elif strategy_name == 'AnotherStrategy':
            param1 = request.param1
            param2 = request.param2
            strategy = AnotherStrategy(param1, param2, funds)
        else:
            # 錯誤處理
            pass

        # 將策略序列化
        serialized_strategy = strategy.SerializeToString()

        # 返回策略
        return strategy_service_pb2.StrategyResponse(strategy=serialized_strategy)

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    strategy_service_pb2_grpc.add_StrategyServiceServicer_to_server(StrategyServicer(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()

if __name__ == '__main__':
    serve()

在這個範例中,我們定義了一個StrategyServicer類別,繼承自自動生成的StrategyServiceServicer類別。在GetStrategy方法中,我們根據客戶端傳遞的參數實例化對應的策略類別,並將策略序列化後返回給客戶端。

在服務端啟動之後,我們需要定義客戶端程式碼,用於將參數傳遞給服務端並獲取策略實

pine語言學習指令碼

//@version=4
study("SMA Trend Strategy")

// 設置參數
fastLength = input(title="Fast Length", type=input.integer, defval=10)
slowLength = input(title="Slow Length", type=input.integer, defval=20)

// 計算指標
fastSMA = sma(close, fastLength)
slowSMA = sma(close, slowLength)

// 設置止盈止損
longStop = strategy.position_avg_price * (1 - 0.01)
shortStop = strategy.position_avg_price * (1 + 0.01)
longlimit = strategy.position_avg_price * (1 + 0.01)
shortlimit = strategy.position_avg_price * (1 - 0.01)

// 開倉條件
longCondition = crossover(fastSMA, slowSMA)
shortCondition = crossunder(fastSMA, slowSMA)

// 開倉
strategy.entry("Long", strategy.long, when=longCondition)
strategy.entry("Short", strategy.short, when=shortCondition)

// 止盈止損
strategy.exit("Long Stop", "Long", stop=longStop,limit=longlimit)
strategy.exit("Short Stop", "Short", stop=shortStop,limit=shortlimit)

The Art of Trading

https://courses.theartoftrading.com/pages/pine-script-mastery-code#strategy1

https://www.youtube.com/watch?v=h-erJbnBj6A&feature=youtu.be

How To Create A Regime Filter

Watch Lesson

// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © ZenAndTheArtOfTrading / www.PineScriptMastery.com
// @version=5
indicator("Regime Filter")

// Get user input
res = input.timeframe(title="Timeframe", defval="D")
len = input.int(title="EMA Length", defval=20)
market = input.symbol(title="Market", defval="NASDAQ:NDX")

// Define custom security function
f_sec(_market, _res, _exp) => request.security(_market, _res, _exp[barstate.isconfirmed ? 0 : 1])

// Get EMA value
ema = ta.ema(close, len)
emaValue = f_sec(market, res, ema)

// Check if price is above or below EMA filter
marketPrice = f_sec(market, res, close)
regimeFilter = marketPrice > emaValue or marketPrice[1] > emaValue[1]

// Change background color
bgcolor(regimeFilter ? color.green : color.red)

AUTO-FIBONACCI Tool

Watch Lesson

// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © ZenAndTheArtOfTrading / PineScriptMastery.com
// @version=5
indicator("Auto-Fib", overlay=true)

// Get user input
var devTooltip          = "Deviation is a multiplier that affects how much the price should deviate from the previous pivot in order for the bar to become a new pivot."
var depthTooltip        = "The minimum number of bars that will be taken into account when calculating the indicator."
threshold_multiplier    = input.float(title="Deviation", defval=3, minval=0, tooltip=devTooltip)
depth                   = input.int(title="Depth", defval=10, minval=1, tooltip=depthTooltip)
reverse                 = input.bool(title="Reverse", defval=false, tooltip="Flips the fibonacci levels around.")
extendLeft              = input.bool(title="Extend Left    |    Extend Right", defval=false, inline="Extend Lines")
extendRight             = input.bool(title="", defval=false, inline="Extend Lines")
prices                  = input.bool(title="Show Prices", defval=false)
deleteLastLine          = input.bool(title="Delete Last Line", defval=true)
levels                  = input.bool(title="Show Levels", defval=true, inline="Levels")
levelsFormat            = input.string(title="", defval="Values", options=["Values", "Percent"], inline="Levels")
labelsPosition          = input.string(title="Labels Position", defval="Left", options=["Left", "Right"])
backgroundTransparency  = input.int(title="Background Transparency", defval=85, minval=0, maxval=100)

// Check extending parameter
var extending = extend.none
if extendLeft and extendRight
    extending := extend.both
if extendLeft and not extendRight
    extending := extend.left
if not extendLeft and extendRight
    extending := extend.right

// Calculate deviation threshold for identifying major swings
dev_threshold = ta.atr(10) / close * 100 * threshold_multiplier

// Prepare pivot variables
var line lineLast = na
var int iLast = 0
var int iPrev = 0
var float pLast = 0
var isHighLast = false // Otherwise the last pivot is a low pivot

// Custom function for detecting pivot points
pivots(src, length, isHigh) =>
    l2 = length * 2
    c = nz(src[length])
    ok = true
    for i = 0 to l2
        if isHigh and src[i] > c
            ok := false
        if not isHigh and src[i] < c
            ok := false
    if ok
        [bar_index[length], c]
    else
        [int(na), float(na)]

// Get bar index & price high/low for current pivots
[iH, pH] = pivots(high, depth / 2, true)
[iL, pL] = pivots(low, depth / 2, false)

// Custom function for calculating price deviation
calc_dev(base_price, price) => 100 * (price - base_price) / price

// Custom function for detecting pivots that meet our deviation criteria
pivotFound(dev, isHigh, index, price) =>
    if isHighLast == isHigh and not na(lineLast)
        // New pivot in same direction as last, so update line (ie. trend-continuation)
        if isHighLast ? price > pLast : price < pLast
            line.set_xy2(lineLast, index, price)
            [lineLast, isHighLast]
        else
            [line(na), bool(na)] // No valid pivot detected, return nothing
    else // Reverse the trend/pivot direction (or create the very first line if lineLast is na)
        if math.abs(dev) > dev_threshold
            // Price move is significant - create a new line between the pivot points
            id = line.new(iLast, pLast, index, price, color=color.gray, width=1, style=line.style_dashed)
            [id, isHigh]
        else
            [line(na), bool(na)]

// If bar index for current pivot high is not NA (ie. we have a new pivot):
if not na(iH)
    dev = calc_dev(pLast, pH) // Calculate the deviation from last pivot
    [id, isHigh] = pivotFound(dev, true, iH, pH)
    if not na(id) // If the line has been updated, update price values and delete previous line
        if id != lineLast and deleteLastLine
            line.delete(lineLast)
        lineLast := id
        isHighLast := isHigh
        iPrev := iLast
        iLast := iH
        pLast := pH
else 
    if not na(iL) // If bar index for current pivot low is not NA (ie. we have a new pivot):
        dev = calc_dev(pLast, pL) // Calculate the deviation from last pivot
        [id, isHigh] = pivotFound(dev, false, iL, pL)
        if not na(id) // If the line has been updated, update price values and delete previous line
            if id != lineLast and deleteLastLine
                line.delete(lineLast)
            lineLast := id
            isHighLast := isHigh
            iPrev := iLast
            iLast := iL
            pLast := pL

// Draw fibonacci level as a line and return the line object ID
draw_fib_line(price, col) =>
    var id = line.new(iLast, price, bar_index, price, color=col, width=1, extend=extending)
    if not na(lineLast)
        line.set_xy1(id, line.get_x1(lineLast), price)
        line.set_xy2(id, line.get_x2(lineLast), price)  
	id  

// Draw fibonacci labels
draw_label(price, txt, txtColor) =>
    x = labelsPosition == "Left" ? line.get_x1(lineLast) : not extendRight ? line.get_x2(lineLast) : bar_index
    labelStyle = labelsPosition == "Left" ? label.style_label_right : label.style_label_left
    align = labelsPosition == "Left" ? text.align_right : text.align_left
    labelsAlignStrLeft = txt + '\n ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏ \n'
    labelsAlignStrRight = '       ' + txt + '\n ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏  ‏ \n'
    labelsAlignStr = labelsPosition == "Left" ? labelsAlignStrLeft : labelsAlignStrRight
    var id = label.new(x=x, y=price, text=labelsAlignStr, textcolor=txtColor, style=labelStyle, textalign=align, color=#00000000)
    label.set_xy(id, x, price)
    label.set_text(id, labelsAlignStr)
    label.set_textcolor(id, txtColor)

// Format the given string
format(txt) => " (" + str.tostring(txt, "#.####") + ")"

// Return the formatted label text for Fibonacci levels
label_txt(level, price) =>
    l = levelsFormat == "Values" ? str.tostring(level) : str.tostring(level * 100) + "%"
    (levels ? l : "") + (prices ? format(price) : "")

// Returns true if price is crossing the given fib level
crossing_level(price, fib) => (fib > price and fib < price[1]) or (fib < price and fib > price[1])

// Get starting and ending high/low price of the current pivot (for calculating fib levels)
startPrice = reverse ? line.get_y1(lineLast) : pLast
endPrice = reverse ? pLast : line.get_y1(lineLast)

// Calculate price difference between high and low
iHL = startPrice > endPrice
diff = (iHL ? -1 : 1) * math.abs(startPrice - endPrice)

// Process the given fib level (calculate fib, draw line & label, detect alerts, fill bgcolor between last fib)
processLevel(show, value, colorL, lineIdOther) =>
    if show
	    fibPrice = startPrice + diff * value
		lineId = draw_fib_line(fibPrice, colorL)
        draw_label(fibPrice, label_txt(value, fibPrice), colorL)
        if crossing_level(close, fibPrice) // Trigger alert if price is crossing this fib level
            alert("Autofib: " + syminfo.ticker + " crossing level " + str.tostring(value))
        if not na(lineIdOther) // Fill background color between each fib level
            linefill.new(lineId, lineIdOther, color=color.new(colorL, backgroundTransparency))
		lineId
    else
		lineIdOther

//{=============================================================================
var g_fibs = "Fibonacci Levels"
// Get Fibonacci level user inputs
show_0 = input(true, "", inline = "Level0", group=g_fibs)
value_0 = input(0, "", inline = "Level0", group=g_fibs)
color_0 = input(#787b86, "", inline = "Level0", group=g_fibs)
//------------------------------------------------------------------------------
show_0_236 = input(true, "", inline = "Level0", group=g_fibs)
value_0_236 = input(0.236, "", inline = "Level0", group=g_fibs)
color_0_236 = input(#f44336, "", inline = "Level0", group=g_fibs)
//------------------------------------------------------------------------------
show_0_382 = input(true, "", inline = "Level1", group=g_fibs)
value_0_382 = input(0.382, "", inline = "Level1", group=g_fibs)
color_0_382 = input(#81c784, "", inline = "Level1", group=g_fibs)
//------------------------------------------------------------------------------
show_0_5 = input(true, "", inline = "Level1", group=g_fibs)
value_0_5 = input(0.5, "", inline = "Level1", group=g_fibs)
color_0_5 = input(#4caf50, "", inline = "Level1", group=g_fibs)
//------------------------------------------------------------------------------
show_0_618 = input(true, "", inline = "Level2", group=g_fibs)
value_0_618 = input(0.618, "", inline = "Level2", group=g_fibs)
color_0_618 = input(#009688, "", inline = "Level2", group=g_fibs)
//------------------------------------------------------------------------------
show_0_65 = input(false, "", inline = "Level2", group=g_fibs)
value_0_65 = input(0.65, "", inline = "Level2", group=g_fibs)
color_0_65 = input(#009688, "", inline = "Level2", group=g_fibs)
//------------------------------------------------------------------------------
show_0_786 = input(true, "", inline = "Level3", group=g_fibs)
value_0_786 = input(0.786, "", inline = "Level3", group=g_fibs)
color_0_786 = input(#64b5f6, "", inline = "Level3", group=g_fibs)
//------------------------------------------------------------------------------
show_1 = input(true, "", inline = "Level3", group=g_fibs)
value_1 = input(1, "", inline = "Level3", group=g_fibs)
color_1 = input(#787b86, "", inline = "Level3", group=g_fibs)
//------------------------------------------------------------------------------
show_1_272 = input(false, "", inline = "Level4", group=g_fibs)
value_1_272 = input(1.272, "", inline = "Level4", group=g_fibs)
color_1_272 = input(#81c784, "", inline = "Level4", group=g_fibs)
//------------------------------------------------------------------------------
show_1_414 = input(false, "", inline = "Level4", group=g_fibs)
value_1_414 = input(1.414, "", inline = "Level4", group=g_fibs)
color_1_414 = input(#f44336, "", inline = "Level4", group=g_fibs)
//------------------------------------------------------------------------------
show_1_618 = input(false, "", inline = "Level5", group=g_fibs)
value_1_618 = input(1.618, "", inline = "Level5", group=g_fibs)
color_1_618 = input(#2196f3, "", inline = "Level5", group=g_fibs)
//------------------------------------------------------------------------------
show_1_65 = input(false, "", inline = "Level5", group=g_fibs)
value_1_65 = input(1.65, "", inline = "Level5", group=g_fibs)
color_1_65 = input(#2196f3, "", inline = "Level5", group=g_fibs)
//------------------------------------------------------------------------------
show_2_618 = input(false, "", inline = "Level6", group=g_fibs)
value_2_618 = input(2.618, "", inline = "Level6", group=g_fibs)
color_2_618 = input(#f44336, "", inline = "Level6", group=g_fibs)
//------------------------------------------------------------------------------
show_2_65 = input(false, "", inline = "Level6", group=g_fibs)
value_2_65 = input(2.65, "", inline = "Level6", group=g_fibs)
color_2_65 = input(#f44336, "", inline = "Level6", group=g_fibs)
//------------------------------------------------------------------------------
show_3_618 = input(false, "", inline = "Level7", group=g_fibs)
value_3_618 = input(3.618, "", inline = "Level7", group=g_fibs)
color_3_618 = input(#9c27b0, "", inline = "Level7", group=g_fibs)
//------------------------------------------------------------------------------
show_3_65 = input(false, "", inline = "Level7", group=g_fibs)
value_3_65 = input(3.65, "", inline = "Level7", group=g_fibs)
color_3_65 = input(#9c27b0, "", inline = "Level7", group=g_fibs)
//------------------------------------------------------------------------------
show_4_236 = input(false, "", inline = "Level8", group=g_fibs)
value_4_236 = input(4.236, "", inline = "Level8", group=g_fibs)
color_4_236 = input(#e91e63, "", inline = "Level8", group=g_fibs)
//------------------------------------------------------------------------------
show_4_618 = input(false, "", inline = "Level8", group=g_fibs)
value_4_618 = input(4.618, "", inline = "Level8", group=g_fibs)
color_4_618 = input(#81c784, "", inline = "Level8", group=g_fibs)
//------------------------------------------------------------------------------
show_neg_0_236 = input(false, "", inline = "Level9", group=g_fibs)
value_neg_0_236 = input(-0.236, "", inline = "Level9", group=g_fibs)
color_neg_0_236 = input(#f44336, "", inline = "Level9", group=g_fibs)
//------------------------------------------------------------------------------
show_neg_0_382 = input(false, "", inline = "Level9", group=g_fibs)
value_neg_0_382 = input(-0.382, "", inline = "Level9", group=g_fibs)
color_neg_0_382 = input(#81c784, "", inline = "Level9", group=g_fibs)
//------------------------------------------------------------------------------
show_neg_0_618 = input(false, "", inline = "Level10", group=g_fibs)
value_neg_0_618 = input(-0.618, "", inline = "Level10", group=g_fibs)
color_neg_0_618 = input(#009688, "", inline = "Level10", group=g_fibs)
//------------------------------------------------------------------------------
show_neg_0_65 = input(false, "", inline = "Level10", group=g_fibs)
value_neg_0_65 = input(-0.65, "", inline = "Level10", group=g_fibs)
color_neg_0_65 = input(#009688, "", inline = "Level10", group=g_fibs)
//-----------------------------------------------------------------------------}
//{=============================================================================
// Process each fibonacci level
//==============================================================================
lineId0  = processLevel(show_neg_0_65, value_neg_0_65, color_neg_0_65, line(na))
lineId1  = processLevel(show_neg_0_618, value_neg_0_618, color_neg_0_618, lineId0)
lineId2  = processLevel(show_neg_0_382, value_neg_0_382, color_neg_0_382, lineId1)
lineId3  = processLevel(show_neg_0_236, value_neg_0_236, color_neg_0_236, lineId2)
lineId4  = processLevel(show_0, value_0, color_0, lineId3)
lineId5  = processLevel(show_0_236, value_0_236, color_0_236, lineId4)
lineId6  = processLevel(show_0_382, value_0_382, color_0_382, lineId5)
lineId7  = processLevel(show_0_5, value_0_5, color_0_5, lineId6)
lineId8  = processLevel(show_0_618, value_0_618, color_0_618, lineId7)
lineId9  = processLevel(show_0_65, value_0_65, color_0_65, lineId8)
lineId10 = processLevel(show_0_786, value_0_786, color_0_786, lineId9)
lineId11 = processLevel(show_1, value_1, color_1, lineId10)
lineId12 = processLevel(show_1_272, value_1_272, color_1_272, lineId11)
lineId13 = processLevel(show_1_414, value_1_414, color_1_414, lineId12)
lineId14 = processLevel(show_1_618, value_1_618, color_1_618, lineId13)
lineId15 = processLevel(show_1_65, value_1_65, color_1_65, lineId14)
lineId16 = processLevel(show_2_618, value_2_618, color_2_618, lineId15)
lineId17 = processLevel(show_2_65, value_2_65, color_2_65, lineId16)
lineId18 = processLevel(show_3_618, value_3_618, color_3_618, lineId17)
lineId19 = processLevel(show_3_65, value_3_65, color_3_65, lineId18)
lineId20 = processLevel(show_4_236, value_4_236, color_4_236, lineId19)
lineId21 = processLevel(show_4_618, value_4_618, color_4_618, lineId20)
//-----------------------------------------------------------------------------}

Pivots & Impulsive Moves

Watch Lesson

// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © ZenAndTheArtOfTrading / PineScriptMastery.com
// @version=5
indicator("Pivot Points", overlay=true)

// Get user input
var devTooltip          = "Deviation is a multiplier that affects how much the price should deviate from the previous pivot in order for the bar to become a new pivot."
var depthTooltip        = "The minimum number of bars that will be taken into account when analyzing pivots."
threshold_multiplier    = input.float(title="Deviation", defval=2.5, minval=0, tooltip=devTooltip)
depth                   = input.int(title="Depth", defval=10, minval=1, tooltip=depthTooltip)
deleteLastLine          = input.bool(title="Delete Last Line", defval=false)
bgcolorChange           = input.bool(title="Change BgColor", defval=false)

// Calculate deviation threshold for identifying major swings
dev_threshold = ta.atr(10) / close * 100 * threshold_multiplier

// Prepare pivot variables
var line lineLast = na
var int iLast = 0 // Index last
var int iPrev = 0 // Index previous
var float pLast = 0 // Price last
var isHighLast = false // If false then the last pivot was a pivot low

// Custom function for detecting pivot points (and returning price + bar index)
pivots(src, length, isHigh) =>
    l2 = length * 2
    c = nz(src[length])
    ok = true
    for i = 0 to l2
        if isHigh and src[i] > c // If isHigh, validate pivot high
            ok := false
        if not isHigh and src[i] < c // If not isHigh, validate pivot low
            ok := false
    if ok // If pivot is valid, return bar index + high price value
        [bar_index[length], c]
    else // If pivot is invalid, return na
        [int(na), float(na)]

// Get bar index & price high/low for current pivots
[iH, pH] = pivots(high, depth / 2, true)
[iL, pL] = pivots(low, depth / 2, false)

// Custom function for calculating price deviation for validating large moves
calc_dev(base_price, price) => 100 * (price - base_price) / price

// Custom function for detecting pivots that meet our deviation criteria
pivotFound(dev, isHigh, index, price) =>
    if isHighLast == isHigh and not na(lineLast) // Check bull/bear direction of new pivot
        // New pivot in same direction as last (a pivot high), so update line upwards (ie. trend-continuation)
        if isHighLast ? price > pLast : price < pLast // If new pivot is above last pivot, update line
            line.set_xy2(lineLast, index, price)
            [lineLast, isHighLast]
        else
            [line(na), bool(na)] // New pivot is not above last pivot, so don't update the line
    else // Reverse the trend/pivot direction (or create the very first line if lineLast is na)
        if math.abs(dev) > dev_threshold
            // Price move is significant - create a new line between the pivot points
            id = line.new(iLast, pLast, index, price, color=color.gray, width=1, style=line.style_dashed)
            [id, isHigh]
        else
            [line(na), bool(na)]

// If bar index for current pivot high is not NA (ie. we have a new pivot):
if not na(iH)
    dev = calc_dev(pLast, pH) // Calculate the deviation from last pivot
    [id, isHigh] = pivotFound(dev, true, iH, pH) // Pass the current pivot high into pivotFound() for validation & line update
    if not na(id) // If the line has been updated, update price & index values and delete previous line
        if id != lineLast and deleteLastLine
            line.delete(lineLast)
        lineLast := id
        isHighLast := isHigh
        iPrev := iLast
        iLast := iH
        pLast := pH
else 
    if not na(iL) // If bar index for current pivot low is not NA (ie. we have a new pivot):
        dev = calc_dev(pLast, pL) // Calculate the deviation from last pivot
        [id, isHigh] = pivotFound(dev, false, iL, pL) // Pass the current pivot low into pivotFound() for validation & line update
        if not na(id) // If the line has been updated, update price values and delete previous line
            if id != lineLast and deleteLastLine
                line.delete(lineLast)
            lineLast := id
            isHighLast := isHigh
            iPrev := iLast
            iLast := iL
            pLast := pL

// Get starting and ending high/low price of the current pivot line
startIndex = line.get_x1(lineLast)
startPrice = line.get_y1(lineLast)
endIndex   = line.get_x2(lineLast)
endPrice   = line.get_y2(lineLast)

// Draw top & bottom of impulsive move
topLine    = line.new(startIndex, startPrice, endIndex, startPrice, extend=extend.right, color=color.red)
bottomline = line.new(startIndex, endPrice, endIndex, endPrice, extend=extend.right, color=color.green)
line.delete(topLine[1])
line.delete(bottomline[1])
//plot(startPrice, color=color.green)
//plot(endPrice, color=color.red)

// Do what you like with these pivot values :)
// Keep in mind there will be an X bar delay between pivot price values updating based on Depth setting
dist = math.abs(startPrice - endPrice)
plot(dist, color=color.new(color.purple,100))
bullish = endPrice > startPrice
offsetBG = -(depth / 2)
bgcolor(bgcolorChange ? bullish ? color.new(color.green,90) : color.new(color.red,90) : na, offset=offsetBG)

Tracking Ichimoku Base Line

Watch Lesson

// PineScriptMastery.com
// @version=5
indicator(title="Ichimoku Cloud", shorttitle="Ichimoku", overlay=true)

// Get user input
conversionPeriods       = input.int(9, minval=1, title="Conversion Line Length")
basePeriods             = input.int(26, minval=1, title="Base Line Length")
laggingSpan2Periods     = input.int(52, minval=1, title="Leading Span B Length")
displacement            = input.int(26, minval=1, title="Lagging Span")
donchian(len)           => math.avg(ta.lowest(len), ta.highest(len))
conversionLine          = donchian(conversionPeriods)
baseLine                = donchian(basePeriods)
leadLine1               = math.avg(conversionLine, baseLine)
leadLine2               = donchian(laggingSpan2Periods)

// Draw cloud
plot(conversionLine, color=#2962FF, title="Conversion Line", display=display.none)
plot(baseLine, color=#B71C1C, title="Base Line", display=display.none)
plot(close, offset = -displacement + 1, color=#43A047, title="Lagging Span", display=display.none)
p1 = plot(leadLine1, offset = displacement - 1, color=#A5D6A7, title="Leading Span A", display=display.none)
p2 = plot(leadLine2, offset = displacement - 1, color=#EF9A9A, title="Leading Span B", display=display.none)
fill(p1, p2, color = leadLine1 > leadLine2 ? color.rgb(67, 160, 71, 90) : color.rgb(244, 67, 54, 90), display=display.none)

// Track horizontal baseline
var baseLineSaved = baseLine
if baseLine != baseLine[1]
    baseLineSaved := na
else
    baseLineSaved := baseLine

// Draw baseline
plot(baseLineSaved, color=color.purple, style=plot.style_linebr, title="Base Line Saved")

// Track price trading above/below baseline
isPriceAboveBaseLine = close > baseLineSaved
priceTradingAboveBL = isPriceAboveBaseLine and not na(baseLineSaved)
priceTradingBelowBL = not isPriceAboveBaseLine and not na(baseLineSaved)

// Draw condition
bgcolor(priceTradingAboveBL ? color.new(color.green,80) : na)
bgcolor(priceTradingBelowBL ? color.new(color.red,80) : na)

// Track candle pattern tests of baseline (example purposes - needs more conditions to be actually useful!)
candlePatternBull = priceTradingAboveBL and priceTradingAboveBL[1] and low[1] < baseLineSaved and close > baseLineSaved
candlePatternBear = priceTradingBelowBL and priceTradingBelowBL[1] and high[1] > baseLineSaved and close < baseLineSaved
plotshape(candlePatternBull, style=shape.triangleup, color=color.green, location=location.belowbar)
plotshape(candlePatternBear, style=shape.triangledown, color=color.red)

A Simple Pullback Strategy

Watch Lesson

// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © ZenAndTheArtOfTrading / www.PineScriptMastery.com
// @version=5
strategy("Simple Pullback Strategy", 
     overlay=true, 
     initial_capital=50000,
     default_qty_type=strategy.percent_of_equity, 
     default_qty_value=100, // 100% of balance invested on each trade
     commission_type=strategy.commission.percent, 
     commission_value=0.2)

// Get user input
i_ma1           = input.int(title="MA 1 Length", defval=120, step=10, group="Strategy Parameters", tooltip="Long-term MA")
i_ma2           = input.int(title="MA 2 Length", defval=10, step=10, group="Strategy Parameters", tooltip="Short-term MA")
i_stopPercent   = input.float(title="Stop Loss Percent", defval=0.10, step=0.1, group="Strategy Parameters", tooltip="Failsafe Stop Loss Percent Decline")
i_lowerClose    = input.bool(title="Exit On Lower Close", defval=false, group="Strategy Parameters", tooltip="Wait for a lower-close before exiting above MA2")
i_startTime     = input.time(title="Start Filter", defval=timestamp("01 Jan 1995 13:30 +0000"), group="Time Filter", tooltip="Start date & time to begin searching for setups")
i_endTime       = input.time(title="End Filter", defval=timestamp("1 Jan 2099 19:30 +0000"), group="Time Filter", tooltip="End date & time to stop searching for setups")

// Get indicator values
ma1 = ta.sma(close, i_ma1)
ma2 = ta.sma(close, i_ma2)

// Check filter(s)
f_dateFilter = time >= i_startTime and time <= i_endTime

// Check buy/sell conditions
var float buyPrice = 0
buyCondition    = close > ma1 and close < ma2 and strategy.position_size == 0 and f_dateFilter
sellCondition   = close > ma2 and strategy.position_size > 0 // (not i_lowerClose or close < low[1])
stopDistance    = strategy.position_size > 0 ? ((buyPrice - close) / close) : na
stopPrice       = strategy.position_size > 0 ? buyPrice - (buyPrice * i_stopPercent) : na
stopCondition   = strategy.position_size > 0 and stopDistance > i_stopPercent

// Enter positions
if buyCondition
    strategy.entry(id="Long", direction=strategy.long)

if buyCondition[1]
    buyPrice := open

// Exit positions
if sellCondition or stopCondition
    strategy.close(id="Long", comment="Exit" + (stopCondition ? "SL=true" : ""))
    buyPrice := na

// Draw pretty colors
plot(buyPrice, color=color.lime, style=plot.style_linebr)
plot(stopPrice, color=color.red, style=plot.style_linebr, offset=-1)
plot(ma1, color=color.blue)
plot(ma2, color=color.orange)

MACD Strategy + 2 Profit Targets

Watch Lesson

// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © ZenAndTheArtOfTrading / www.PineScriptMastery.com
//
// System Rules:
// Indicators: MACD indicator (default values), ATR (14), EMA (200)
// 1. Price must be trading above/below the 200 EMA
// 2. Only 1 bar can close above/below the 200 EMA over the past 5 bars
// 3. We take the FIRST MACD cross and ignore subsequent signals until TP/SL is hit
// 4. Stop Loss = 0.5 ATR above/below the recent swing high/low (7 bars lookback)
// 5. First take profit = 1:1 (25%)
// 6. Second take profit = 2:1 (100%)
// 7. Move stop loss to break-even after 1st target is hit
//
// @version=5
strategy("[2022] MACD Cross Strategy", overlay=true,
     currency="USD",
     calc_on_order_fills=true,
     use_bar_magnifier=true,
     initial_capital=10000,
     default_qty_type=strategy.percent_of_equity,
     default_qty_value=100, // 100% of balance invested on each trade
     commission_type=strategy.commission.cash_per_contract)
     //commission_value=0.005) // Interactive Brokers rate

// Import ZenLibrary
import ZenAndTheArtOfTrading/ZenLibrary/5 as zen

// Get user input
var g_system    = "System Entry Settings"
i_ema_filter    = input.int(title="EMA Filter Length", defval=200, group=g_system)
i_ema_filter2   = input.int(title="EMA Max Bars Above/Below", defval=1, group=g_system)
i_stop_multi    = input.float(title="Stop Loss Multiplier", defval=0.5, step=0.5, group=g_system)
i_stop_lookback = input.int(title="Stop Loss Lookback", defval=7, group=g_system)
var g_risk      = "System Risk Settings"
i_rr1           = input.float(title="Risk:Reward Target 1", defval=1.0, group=g_risk)
i_rr2           = input.float(title="Risk:Reward Target 2", defval=2.0, group=g_risk)
i_target1       = input.float(title="Profit % Target 1", defval=25, group=g_risk)
i_riskPerTrade  = input.float(title="Forex Risk Per Trade %", defval=1.0)
var g_macd      = "MACD Settings"
i_price_src     = input.source(title="Price Source", defval=close, group=g_macd)
i_fast_length   = input.int(title="Fast Length", defval=12, group=g_macd)
i_slow_length   = input.int(title="Slow Length", defval=26, group=g_macd)
i_signal_length = input.int(title="Signal Smoothing", minval=1, maxval=50, defval=9, group=g_macd)
i_sma_source    = input.string(title="Oscillator MA Type", defval="EMA", options=["SMA", "EMA"], group=g_macd)
i_sma_signal    = input.string(title="Signal Line MA Type", defval="EMA", options=["SMA", "EMA"], group=g_macd)

//------------- DETERMINE CURRENCY CONVERSION RATE -------------//
// Check if our account currency is the same as the base or quote currency or neither (for risk $ conversion purposes)
accountSameAsCounterCurrency = strategy.account_currency == syminfo.currency
accountSameAsBaseCurrency = strategy.account_currency == syminfo.basecurrency
accountNeitherCurrency = not accountSameAsCounterCurrency and not accountSameAsBaseCurrency
// Get currency conversion rates if applicable
conversionCurrencyPair = accountSameAsCounterCurrency ? syminfo.tickerid : strategy.account_currency + syminfo.currency
conversionCurrencyRate = accountSameAsBaseCurrency or accountNeitherCurrency ? request.security(conversionCurrencyPair, "D", close, ignore_invalid_symbol=true) : 1.0
// Display the current conversion currency ticker (for debug purposes)
if barstate.islastconfirmedhistory
    table t = table.new(position.top_right, 1, 2, color.black)
    table.cell(t, 0, 0, "Conversion: " + conversionCurrencyPair + " (" + str.tostring(conversionCurrencyRate) + ")", text_color=color.white, text_size=size.small)
    table.cell(t, 0, 1, "Account: $" + str.tostring(zen.truncate(strategy.equity)), text_color=color.white, text_size=size.small)
//------------- END CURRENCY CONVERSION RATE CODE -------------//

// Calculate MACD
[macdLine, signalLine, histLine] = ta.macd(i_price_src, i_fast_length, i_slow_length, i_signal_length)

// Get indicator values
ema = ta.ema(close, i_ema_filter)
atr = ta.atr(14)

// Check for zero-point crosses
crossUp     = ta.crossover(signalLine, macdLine)
crossDown   = ta.crossunder(signalLine, macdLine)

// Check general system filters
tradeFilters = not na(ema) and not na(atr)

// Check trend conditions
upTrend     = close > ema
downTrend   = close < ema

// Check trade conditions
longConditions  = tradeFilters and macdLine[1] < 0 and signalLine[1] < 0
shortConditions = tradeFilters and macdLine[1] > 0 and signalLine[1] > 0

// Confirm long & short setups
longSignal   = longConditions and upTrend and crossDown
shortSignal  = shortConditions and downTrend and crossUp

// Calculate stop loss
longStop    = ta.lowest(low, i_stop_lookback) - (atr * i_stop_multi)
shortStop   = ta.highest(high, i_stop_lookback) + (atr * i_stop_multi)

// Save stops & targets
var float tradeStop = na
var float tradeTarget1 = na
var float tradeTarget2 = na
var float tradeSize = na

// Count bars above/below MA
int barsAboveMA = 0
int barsBelowMA = 0

for i = 1 to 5
    if close[i] < ema[i]
        barsBelowMA += 1
    if close[i] > ema[i]
        barsAboveMA += 1

// Combine signal filters
longTrade   = longSignal  and barsBelowMA <= i_ema_filter2 and strategy.position_size == 0
shortTrade  = shortSignal and barsAboveMA <= i_ema_filter2 and strategy.position_size == 0

// Handle long trade entry (enter position, reset stops & targets)
if longTrade
    if syminfo.type == "forex"
        tradeStop := longStop
        stopDistance = close - tradeStop
        tradeTarget1 := close + (stopDistance * i_rr1)
        tradeTarget2 := close + (stopDistance * i_rr2)
        tradeSize := na
        positionSize = zen.av_getPositionSize(strategy.equity, i_riskPerTrade, zen.toWhole(stopDistance) * 10, conversionCurrencyRate)
        strategy.entry(id="Long", direction=strategy.long, qty=positionSize)
    else
        strategy.entry(id="Long", direction=strategy.long)
        tradeStop := na
        tradeTarget1 := na
        tradeTarget2 := na

// Handle short trade entry (enter position, reset stops & targets)
if shortTrade
    if syminfo.type == "forex"
        tradeStop := shortStop
        stopDistance = tradeStop - close
        tradeTarget1 := close - (stopDistance * i_rr1)
        tradeTarget2 := close - (stopDistance * i_rr2)
        tradeSize := na
        positionSize = zen.av_getPositionSize(strategy.equity, i_riskPerTrade, zen.toWhole(shortStop - close) * 10, conversionCurrencyRate)
        strategy.entry(id="Short", direction=strategy.short, qty=positionSize)
    else
        strategy.entry(id="Short", direction=strategy.short)
        tradeStop := na
        tradeTarget1 := na
        tradeTarget2 := na
    
// Handle forex trade size tracking variable
if syminfo.type == "forex" and strategy.position_size != 0 and na(tradeSize)
    tradeSize := strategy.position_size

// Handle long stops & target calculation
if strategy.position_size > 0 and na(tradeStop) and syminfo.type != "forex"
    tradeStop := longStop
    stopDistance = strategy.position_avg_price - tradeStop
    tradeTarget1 := strategy.position_avg_price + (stopDistance * i_rr1)
    tradeTarget2 := strategy.position_avg_price + (stopDistance * i_rr2)
    tradeSize := strategy.position_size

// Handle short stops & target calculation
if strategy.position_size < 0 and na(tradeStop) and syminfo.type != "forex"
    tradeStop := shortStop
    stopDistance = tradeStop - strategy.position_avg_price
    tradeTarget1 := strategy.position_avg_price - (stopDistance * i_rr1)
    tradeTarget2 := strategy.position_avg_price - (stopDistance * i_rr2)
    tradeSize := strategy.position_size

// Handle trade exits
strategy.exit(id="Long Exit #1",  from_entry="Long",  limit=tradeTarget1, stop=tradeStop, qty_percent=i_target1)
strategy.exit(id="Long Exit #2",  from_entry="Long",  limit=tradeTarget2, stop=tradeStop, qty_percent=100)
strategy.exit(id="Short Exit #1", from_entry="Short", limit=tradeTarget1, stop=tradeStop, qty_percent=i_target1)
strategy.exit(id="Short Exit #2", from_entry="Short", limit=tradeTarget2, stop=tradeStop, qty_percent=100)

// Handle both long & short trade break-even stops (do this AFTER first position has exited above ^)
if strategy.position_size != tradeSize
    tradeStop := strategy.position_avg_price
    tradeTarget1 := na

// Draw conditional data
plot(ema, color=close > ema ? color.green : color.red, linewidth=2, title="EMA")
plotshape(longTrade,  style=shape.triangleup,   color=color.green, location=location.belowbar, title="Long Setup")
plotshape(shortTrade, style=shape.triangledown, color=color.red,   location=location.abovebar, title="Short Setup")

// Draw stops & targets
plot(strategy.position_size != 0 ? tradeStop : na,   color=color.red,   style=plot.style_linebr, title="Stop Loss")
plot(strategy.position_size != 0 ? tradeTarget1 : na, color=color.green, style=plot.style_linebr, title="Profit Target 1")
plot(strategy.position_size != 0 ? tradeTarget2 : na, color=color.green, style=plot.style_linebr, title="Profit Target 2")

Calculating Forex LOT SIZES

Watch Lesson

// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © ZenAndTheArtOfTrading / www.PineScriptMastery.com
//
// System Rules:
// Indicators: MACD indicator (default values), ATR (14), EMA (200)
// 1. Price must be trading above/below the 200 EMA
// 2. Only 1 bar can close above/below the 200 EMA over the past 5 bars
// 3. We take the FIRST MACD cross and ignore subsequent signals until TP/SL is hit
// 4. Stop Loss = 0.5 ATR above/below the recent swing high/low (7 bars lookback)
// 5. First take profit = 1:1 (25%)
// 6. Second take profit = 2:1 (100%)
// 7. Move stop loss to break-even after 1st target is hit
//
// @version=5
strategy("[2022] MACD Cross Strategy", overlay=true,
     currency="USD",
     calc_on_order_fills=true,
     use_bar_magnifier=true,
     initial_capital=10000,
     default_qty_type=strategy.percent_of_equity,
     default_qty_value=100, // 100% of balance invested on each trade
     commission_type=strategy.commission.cash_per_contract)
     //commission_value=0.005) // Interactive Brokers rate

// Get user input
var g_system    = "System Entry Settings"
i_ema_filter    = input.int(title="EMA Filter Length", defval=200, group=g_system)
i_ema_filter2   = input.int(title="EMA Max Bars Above/Below", defval=1, group=g_system)
i_stop_multi    = input.float(title="Stop Loss Multiplier", defval=0.5, step=0.5, group=g_system)
i_stop_lookback = input.int(title="Stop Loss Lookback", defval=7, group=g_system)
var g_risk      = "System Risk Settings"
i_rr1           = input.float(title="Risk:Reward Target 1", defval=1.0, group=g_risk)
i_rr2           = input.float(title="Risk:Reward Target 2", defval=2.0, group=g_risk)
i_target1       = input.float(title="Profit % Target 1", defval=25, group=g_risk)
i_riskPerTrade  = input.float(title="Forex Risk Per Trade %", defval=1.0)
i_useLots       = input.bool(title="Use Lots Instead Of Units", defval=false)
var g_macd      = "MACD Settings"
i_price_src     = input.source(title="Price Source", defval=close, group=g_macd)
i_fast_length   = input.int(title="Fast Length", defval=12, group=g_macd)
i_slow_length   = input.int(title="Slow Length", defval=26, group=g_macd)
i_signal_length = input.int(title="Signal Smoothing", minval=1, maxval=50, defval=9, group=g_macd)
i_sma_source    = input.string(title="Oscillator MA Type", defval="EMA", options=["SMA", "EMA"], group=g_macd)
i_sma_signal    = input.string(title="Signal Line MA Type", defval="EMA", options=["SMA", "EMA"], group=g_macd)

//------------- DETERMINE CURRENCY CONVERSION RATE ------------- { //
// Import ZenLibrary
import ZenAndTheArtOfTrading/ZenLibrary/5 as zen
// Custom function for converting units into lot sizes
unitsToLots(units) =>
    float lots = units / 100000
    lots := math.round(lots, 2)
    _return = lots * 100000
// Check if our account currency is the same as the base or quote currency or neither (for risk $ conversion purposes)
accountSameAsCounterCurrency = strategy.account_currency == syminfo.currency
accountSameAsBaseCurrency = strategy.account_currency == syminfo.basecurrency
accountNeitherCurrency = not accountSameAsCounterCurrency and not accountSameAsBaseCurrency
// Get currency conversion rates if applicable
conversionCurrencyPair = accountSameAsCounterCurrency ? syminfo.tickerid : strategy.account_currency + syminfo.currency
conversionCurrencyRate = accountSameAsBaseCurrency or accountNeitherCurrency ? request.security(conversionCurrencyPair, "D", close, ignore_invalid_symbol=true) : 1.0
// Display the current conversion currency ticker (for debug purposes)
if barstate.islastconfirmedhistory
    table t = table.new(position.top_right, 1, 2, color.black)
    table.cell(t, 0, 0, "Conversion: " + conversionCurrencyPair + " (" + str.tostring(conversionCurrencyRate) + ")", text_color=color.white, text_size=size.small)
    table.cell(t, 0, 1, "Account: $" + str.tostring(zen.truncate(strategy.equity)), text_color=color.white, text_size=size.small)
//------------- END CURRENCY CONVERSION RATE CODE ------------- }//

// Calculate MACD
[macdLine, signalLine, histLine] = ta.macd(i_price_src, i_fast_length, i_slow_length, i_signal_length)

// Get indicator values
ema = ta.ema(close, i_ema_filter)
atr = ta.atr(14)

// Check for zero-point crosses
crossUp     = ta.crossover(signalLine, macdLine)
crossDown   = ta.crossunder(signalLine, macdLine)

// Check general system filters
tradeFilters = not na(ema) and not na(atr)

// Check trend conditions
upTrend     = close > ema
downTrend   = close < ema

// Check trade conditions
longConditions  = tradeFilters and macdLine[1] < 0 and signalLine[1] < 0
shortConditions = tradeFilters and macdLine[1] > 0 and signalLine[1] > 0

// Confirm long & short setups
longSignal   = longConditions and upTrend and crossDown
shortSignal  = shortConditions and downTrend and crossUp

// Calculate stop loss
longStop    = ta.lowest(low, i_stop_lookback) - (atr * i_stop_multi)
shortStop   = ta.highest(high, i_stop_lookback) + (atr * i_stop_multi)

// Save stops & targets
var float tradeStop = na
var float tradeTarget1 = na
var float tradeTarget2 = na
var float tradeSize = na

// Count bars above/below MA
int barsAboveMA = 0
int barsBelowMA = 0

for i = 1 to 5
    if close[i] < ema[i]
        barsBelowMA += 1
    if close[i] > ema[i]
        barsAboveMA += 1

// Combine signal filters
longTrade   = longSignal  and barsBelowMA <= i_ema_filter2 and strategy.position_size == 0
shortTrade  = shortSignal and barsAboveMA <= i_ema_filter2 and strategy.position_size == 0

// Handle long trade entry (enter position, reset stops & targets)
if longTrade
    if syminfo.type == "forex"
        tradeStop := longStop
        stopDistance = close - tradeStop
        tradeTarget1 := close + (stopDistance * i_rr1)
        tradeTarget2 := close + (stopDistance * i_rr2)
        tradeSize := na
        positionSize = zen.av_getPositionSize(strategy.equity, i_riskPerTrade, zen.toWhole(stopDistance) * 10, conversionCurrencyRate)
        strategy.entry(id="Long", direction=strategy.long, qty=i_useLots ? unitsToLots(positionSize) : positionSize)
    else
        strategy.entry(id="Long", direction=strategy.long)
        tradeStop := na
        tradeTarget1 := na
        tradeTarget2 := na

// Handle short trade entry (enter position, reset stops & targets)
if shortTrade
    if syminfo.type == "forex"
        tradeStop := shortStop
        stopDistance = tradeStop - close
        tradeTarget1 := close - (stopDistance * i_rr1)
        tradeTarget2 := close - (stopDistance * i_rr2)
        tradeSize := na
        positionSize = zen.av_getPositionSize(strategy.equity, i_riskPerTrade, zen.toWhole(shortStop - close) * 10, conversionCurrencyRate)
        strategy.entry(id="Short", direction=strategy.short, qty=i_useLots ? unitsToLots(positionSize) : positionSize)
    else
        strategy.entry(id="Short", direction=strategy.short)
        tradeStop := na
        tradeTarget1 := na
        tradeTarget2 := na
    
// Handle forex trade size tracking variable
if syminfo.type == "forex" and strategy.position_size != 0 and na(tradeSize)
    tradeSize := strategy.position_size

// Handle long stops & target calculation
if strategy.position_size > 0 and na(tradeStop) and syminfo.type != "forex"
    tradeStop := longStop
    stopDistance = strategy.position_avg_price - tradeStop
    tradeTarget1 := strategy.position_avg_price + (stopDistance * i_rr1)
    tradeTarget2 := strategy.position_avg_price + (stopDistance * i_rr2)
    tradeSize := strategy.position_size

// Handle short stops & target calculation
if strategy.position_size < 0 and na(tradeStop) and syminfo.type != "forex"
    tradeStop := shortStop
    stopDistance = tradeStop - strategy.position_avg_price
    tradeTarget1 := strategy.position_avg_price - (stopDistance * i_rr1)
    tradeTarget2 := strategy.position_avg_price - (stopDistance * i_rr2)
    tradeSize := strategy.position_size

// Handle trade exits
float exitPartialUnits = math.abs(strategy.position_size / (100 / i_target1))
float exitPartialLots = unitsToLots(exitPartialUnits)
strategy.exit(id="Long Exit #1",  from_entry="Long",  limit=tradeTarget1, stop=tradeStop, qty=i_useLots ? exitPartialLots : exitPartialUnits)
strategy.exit(id="Long Exit #2",  from_entry="Long",  limit=tradeTarget2, stop=tradeStop, qty_percent=100)
strategy.exit(id="Short Exit #1", from_entry="Short", limit=tradeTarget1, stop=tradeStop, qty=i_useLots ? exitPartialLots : exitPartialUnits)
strategy.exit(id="Short Exit #2", from_entry="Short", limit=tradeTarget2, stop=tradeStop, qty_percent=100)

// Handle both long & short trade break-even stops (do this AFTER first position has exited above ^)
if strategy.position_size != tradeSize
    tradeStop := strategy.position_avg_price
    tradeTarget1 := na

// Draw conditional data
plot(ema, color=close > ema ? color.green : color.red, linewidth=2, title="EMA")
plotshape(longTrade,  style=shape.triangleup,   color=color.green, location=location.belowbar, title="Long Setup")
plotshape(shortTrade, style=shape.triangledown, color=color.red,   location=location.abovebar, title="Short Setup")

// Draw stops & targets
plot(strategy.position_size != 0 ? tradeStop : na,   color=color.red,   style=plot.style_linebr, title="Stop Loss")
plot(strategy.position_size != 0 ? tradeTarget1 : na, color=color.green, style=plot.style_linebr, title="Profit Target 1")
plot(strategy.position_size != 0 ? tradeTarget2 : na, color=color.green, style=plot.style_linebr, title="Profit Target 2")

Mean Reversion Strategy

Watch Lesson

// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © ZenAndTheArtOfTrading / www.PineScriptMastery.com
// @version=5
strategy("ATR Reversion System", 
     overlay=true, 
     currency=currency.USD,
     initial_capital=100000, 
     default_qty_type=strategy.percent_of_equity, 
     default_qty_value=100, 
     commission_type=strategy.commission.cash_per_order, 
     commission_value=9.95)

// Get user input
i_EmaLongLength     = input.int(title="Long-term EMA", defval=200)
i_EmaShortLength    = input.int(title="Short-term EMA Length", defval=20)
i_ATRPeriod         = input.int(title="ATR Period", defval=5)
i_ATRBand           = input.float(title="ATR Band Distance", defval=1)
i_ATRStretch        = input.float(title="ATR Buy Stretch", defval=1)
i_SellBand          = input.string(title="Sell At Band:", defval="Middle", options=["Top", "Middle", "Bottom"])
i_SellSrc           = input.source(title="Sell Price Source", defval=high)

// Get indicator values
emaLongTerm     = ta.ema(close, i_EmaLongLength)
emaShortTerm    = ta.ema(close, i_EmaShortLength)
atrValue        = ta.atr(i_ATRPeriod)

// Get ATR bands
atrBandTop = emaShortTerm + (atrValue * i_ATRBand)
atrBandBot = emaShortTerm - (atrValue * i_ATRBand)

// Define price stretch
float buyLimitPrice = na

// Check setup conditions = bar close is below ATR band, above long-term EMA
setupCondition = close < atrBandBot and low > emaLongTerm

// Clear any pending limit orders
strategy.cancel_all()

// Enter trades on next bar after setup condition is met
if setupCondition
    buyLimitPrice := low - (atrValue * i_ATRStretch)
    strategy.entry("Long", strategy.long, limit=buyLimitPrice)

// Get sell price
sellPrice = switch i_SellBand
    "Top"       => atrBandTop
    "Middle"    => emaShortTerm
    "Bottom"    => atrBandBot

// Exit trades
if i_SellSrc >= sellPrice or close < emaLongTerm
    strategy.close("Long", comment="Exit trade")

// Draw data to chart
plot(emaLongTerm, "EMA Filter", color.red, 2)
plot(emaShortTerm, "ATR Band Middle", color.blue)
plot(atrBandBot, "ATR Band Bottom", color=color.green)
plot(atrBandTop, "ATR Band Top", color=color.new(color.gray, 75))
plot(setupCondition ? buyLimitPrice : na, "Buy Limit", color.lime, 1, plot.style_cross)

拾人牙慧

判斷趨勢

  • 美股 臺股 亞股 歐股 一起看
  • 臺股上市櫃指數站上月線才做多

選股

  • 美股會領先反應誰是主流類股 ,當臺股某股票發動時去跟美股主流股比較是否是相同類股

    • ex: 2023/01~2023/05 Nvidia 漲100% ,臺股AI相關股票5月發動也是Nvidia 受惠股
  • 選產業未來

  • 臺股找產業未來 or 題材

  • 看月K or 周K 長紅棒超過12根漲幅% 該股可能後面有大行情

  • 阿魯米

    • 空頭選股跟大盤比,哪些股票先回到起跌時候的股價
    • 個股漲幅需比大盤強,找出強勢股名單
  • 張捷

    • 「買對(個股),押大(部位),長抱!」
    • 張捷口中的「長抱」,指的是波段投資,個股買進後平均持有時間約3個月,講「長抱」的用意是提醒學員不要做太短,因為當沖的短線交易成本(手續費、證交稅)太高,短線的買賣點也會因誤判而無法精準掌握,賣掉後可能又被迫追高買回來,把自己搞得緊張兮兮,甚至嚴重影響心情,得不償失。
    • 產業趨勢一旦成形,可能持續1、2年,甚至更久,但該產業成長股的股價通常會提前反映,而且漲勢通常會集中在幾個波段,張捷因此認為,投資人若能領先掌握產業及相關個股的成長動能,在相對低檔進場做波段投資,會比買進個股長抱1年以上更有效率。
  • 奇正

    • 買進強勢類股中的最強勢股票
      • 依據我多年的觀察與驗證,在強勢類股中,強勢股與弱勢股表現也是差很多;而漲最凶的通常不是基本面最好的,所以你如果以基本面去選,而不是以強弱去選,很可能買到的都不會漲,別人漲一倍兩倍,你是漲10%,20%。以近期的「美國寬頻基建概念股」來說,華星光、仲琦是類股中其他股票漲幅的數倍,到底在漲什麼?就像AI為什麼會漲到緯創,不就是一代工廠嗎?也能飆成就好像它是OPEN AI在臺灣上市,總之,股市就是無釐頭啦!簡而言之,股市黃金律就是「買進強勢類股中的最強勢股票」不是買進最好的股票。對於10個裡面有9.9999個不懂得這個道理的股市小白你叫他去開槓桿快速致富,是要害人吧!
    • 專心致力研究當時表現最突出的股票
      • 為什麼李佛摩是投機的祖師爺,因為他所講過的話到最後都被驗證是至理名言。追隨主流股,別不信邪,什麼技術分析、籌碼分析、產業分析、宇宙奧祕、包寧傑通道…,攏總沒路用。唯一有用的就只有,辨識主流股。「…專心致力研究當時表現最突出的股票。假如你無法從表現領先的熱門股身上賺到錢,你就別想從整個股市裡賺到錢。正如女人的服裝、帽子與珠寶永遠都在變一樣,股市中的主流類股也是此起彼落的變換。多年以前的主流類股是鐵路股、美國糖業和煙草類股。再來換成鋼鐵,美國糖業和煙草類股則悄悄走入歷史。按著汽車類股出現,這樣的戲碼至今仍不斷上演。…」

當沖 (阿老師)

  • 賭紅K 當沖
    • https://www.youtube.com/watch?v=gg2VILaU3jc
    • 開盤價是一個關鍵點
    • V 轉的低點要爆量往上
    • 在開盤價以下不能太久時間 反之賭黑K 也是不能在開盤價之上太久
    • 在開盤價上面遊走很久收紅K機會大

隔日沖 (阿老師)

  • https://www.youtube.com/watch?v=003FKXkyNII
  • 隔日沖屬於波段單的範疇
  • 隔日沖位階很重要
  • 乖離過大 不要做隔日沖
  • 波段單建立部位最好是在短期波段突破不一定要長紅K 只要是紅K 收盤價是最近幾日最高點 (也適合當天收盤買做隔日沖)

資金配置

  • https://www.youtube.com/watch?v=mV-hhdsINyI
  • 各種資產配置
    • 股票集中管理3~5檔 ,操作不同產業股票或同產業不同屬性
      • ex: 1000萬 分3檔股票操作,資金分10等份 一次丟一份100~150萬
    • 同產業不太一樣~ 或是產業不同

暴大量

  • 暴大量隔天是觀察日

  • 低檔爆量

    • 上攻力道強
  • 高檔暴大量

    • 換手失敗長黑K,連續K 可能就是頭部形成,可能要休息一陣子
    • 換手成功上去壓力變支撐

進場

  • 觀察位階,漲過一波位階高要小心
  • 低檔暴大量
  • 高檔暴大量換手區,換手成功變支撐
  • 發動後第一波沒跟上等盤整後發動第一根尾盤買盈虧比會漂亮
  • 多頭股價上漲一般都是走階梯
  • 上市櫃指數站上月線 or 10日均線 or 突破盤整
    • 檢視抗跌或率先突破的細產業,並跟大盤比較,找出強勢族群&又有量 https://www.youtube.com/watch?v=0c4D-3Bh6M0
  • 阿魯米
    • 波段單建立部位最好是在短期波段突破收盤價是最近幾日最高點(不一定要長紅K,只要是紅K)
    • 量縮到極致,之後慢慢出量上漲可能是發動前兆 https://galaxyinvestment.my-galaxy.com.tw/GalaxyAcademy/groupCourse/groupCourse.do?id=20230104464156

出場

  • 大盤指數周K出現第一根轉折開始分批賣,飆股可能會拉到最高價(出現最高上引線) or 暴大量之後開始倒貨
  • 等幅會跟上一波大於等於上波漲幅去觀察
  • 進場沒獲利情況設 5~10%停損,當獲利10% 以上移動停利或周K翻黑出場
  • 停損設突破長紅棒最低點
  • 停損跳空上漲缺口區 (回補缺口代表弱掉)
  • 高檔暴大量,換手常常伴隨大黑K
  • 期貨當天噴出去瘋狂時候賣

加碼(建倉)

  • 現股與期貨不同

    • 現貨:需要交割,沒賣出股票之前,錢回不來,資金要分配分批加碼建倉

    • 期貨:期貨只要有交易,保證金可以滾入再交易,可以把賺來錢再加碼

  • 當沖加碼不適合加碼太多次 ex: 試單對了之後關鍵點位 all in 再減碼出場

  • 金字塔加碼 https://www.youtube.com/watch?v=JI9uSxGu2_U

    • 做波段資金要分配,所以要分批加碼建倉

    • 正金字塔順勢交易,越買越少,往上加碼越少成本不會墊高太多,底倉比較大 ex: 10口 8口 3口 1口,加碼4次

    • 倒金字塔逆勢交易 (存股族),越買越多,往下加碼 ex: 指數型 ETF

  • 加碼次數跟行情有關係,臺股可以預設分三批加碼

  • JG

    • 趨勢多頭格局,在拉回收黑都是進場加碼的時候

    • 分批買進,只是建倉,壓低成本,抱得住後面才能暴賺

    • 美股要拆太多筆(10筆) ,可以向下分批

    • 美股震盪大15~20% 才加碼一次

    • 假設分三批進場,在三批全部打進去之前都視為建倉,所以在完成建倉之前,風險都控制在10元,所以在第二筆打進去之後,成本110應該是在105的時候就會止損了,"有賺不能賠"應該是建倉完畢後才開始執行

    • 前期建倉完成後是用一個沒有賠到錢心情在拼後面大利潤

    • 每後一次進場風險都變小

    • 暴賺哲學 選股標的同時持有不同股票

  • 10倍股法則 林子揚

    • 金字塔分批,越買越少

波段

  • 阿魯米

    • 空頭短線必勝法 順勢交易 https://www.youtube.com/watch?v=FF49VYNzFYA

      • 要有停損點

      • 要有參考點 (參考點右側是順勢交易,左側是逆勢交易)

        • 確定低點出來才進去買,確定高點出來才去賣
      • 擷取選取區域_040

      • 10天以上超過1周就要看周K方向

      • 擷取選取區域_041

      • 主級別上一層來判斷方向

        • ex: 做3~10日 <-主級別是60分K

          • 日K方向往上
          • 主級別是60分,上個級別(日)看方向,下個級別(15分)找進場點
          • 小級別是15分,做停損架構,去享受大級別的方向,當它推動走出去是日線的收獲,15分K的停損
        • ex:做10~60日 主級別是日K

          • 周K看趨勢方向
          • 主級別看型態結構
          • 小級別從微觀看進出位置
      • 設定R值

        • R值:進場點 ~ 停損點
        • 目標區: 2~3倍R,在目標區之前設定停損即可,之後 follow trend
        • 擷取選取區域_042
    • 買在波段起漲點

    • 買在最具有動能的地方

    • 盤區偏高地方是危險

    • 證偽循環的步驟

      • 趨勢猜測

      • 趨勢證明

      • 追蹤趨勢加減碼

      • 停損價在總部位的均價以上

        • 一旦涉及加碼移動停損點要在成本上面不能虧錢
      • 出場原則

    • 在不確定性中,尋找確定性

  • 波段先看大環境 美國 臺灣 經濟 & 大盤指數

  • 做多:

    • 多頭/空頭/盤整都需要跟大盤相比都要超過大盤
  • 股票波段噴出大紅K價位點,可能都不容易回來,不會回頭的價位點通常都是噴出的價位點

  • 波段加減碼 or 空手 看櫃買指數跟大盤指數

    • 上市上櫃指數站上月線 or 10MA
      • 初升段 -> 主升段力道最強,後面出現盤整可能是末升段,資金要控制縮小,提防回檔修正
    • 波浪理論
      • 初升段跟主升段資金可以放大點
  • 波段單建立部位最好是在短期波段突破不一定要長紅K 只要是紅K 收盤價是最近幾日最高點

  • 想辦法找出當下主流股

    • 真正主流股美股通常先會反應 ex: NVIDIA 2023/01~2023/5 漲100% 臺股2023/5月才開始發動
  • 做短線靠技巧,做波段靠天吃飯

  • 做波段沒走出來不知道是波段終點在哪 ~ 波段資金是慢慢加碼不是一次買

  • 周K/月K 長紅棒(超過過去12根漲幅) 可能後面有大行情 ,日K級別找進行點

  • 發動中股票之後盤整均線糾結後第一根長紅棒尾盤買進, 盈虧比漂亮

抄底

  • BNF
  • 郭泰小抄底
    • 選找未來3~6個月主流趨勢類股,哪檔最強
    • 皮球理論
  • 權證小哥
    • 急跌大綠棒爆大量進場賭反彈
      • 馬丁策略賭10次 ex: 第一次10元 失敗,第二次20元 , 第三次 40元,依此類推

實戰

  • 股票操作

    • 臺股是淺碟型市場,漲時容易超漲,跌時也容易超跌
    • 一波 週期是 3~8 週走勢就進入盤整
    • RSI 80 連續三天符合的股票檔數多寡判斷大盤強弱
    • 進場
      • 大盤周K在連續黑K後轉紅K 試單 -> 看對-> 加碼
    • 出場
      • 周K轉綠 減碼 or 賣出
  • 選擇權操作

    • 大盤周K 出現轉折
      • 連續紅or綠轉折
    • 選擇權買價外賭轉折後噴出
    • 期貨指數 60分60MA 穿越
    • 履約價CALL or PUT 最低價20元左右買

總結

  • 多頭行情想辦法找出主流股
  • 嘗試不斷高檔減碼、低檔買回、高檔減碼、再低檔買回 by JG
  • 沒行情
    • 空手
    • 盤整小資金練功能獲利
  • 大賺小賠 (控制賠)
  • 資金配置
  • 嚴格停損
  • 停利?
  • 看對 壓力 抱緊 <- 使用加碼
  • 心態 : 資金風控 & 停損 & 紀律 & 停損相關

高拋大

最近對高拋大的這段話特別有感:
多頭行情
你看到的壓力,都不會是壓力
空頭行情
你預判的支持,都不會有撐
所以
多頭看撐不看壓
空頭看壓不看撐。
多頭行情,買黑不買紅
空頭行情,高拋又低吸」
我的理解是
多頭行情不可在壓力點放空
容易被軋空
空頭行情不可在支撐點作多
容易被套牢
多頭行情要在支撐點買進
空頭行情要在壓力點放空
多頭行情最好是作長多,才賺的多
要作空只能短空,賺了就跑不宜久
空
空頭行情最好是作長空,才賺的多
要作多隻能短多,賺了就跑不宜久
多
這理論是指個股而言
問題是如何判斷多、空行情
可從大盤是多或空判斷
可從個股的位階,是初升段或中段或連續噴出的尾段判斷
可從成交量是否連續增加或連續減少判斷
可從業績是否連續增加或衰退判斷
可從產業是否多、空頭判斷
可從媒體報導中利多或利空消息判斷
可從法人的買、賣超數判斷
可從是否有現增或庫藏股判斷
可從政府的政策立場判斷
其他等等因素.......
這是我對高拋大者段話的理解,不知道是否正確?
從群友藍雨、E神的發文中得知最近的盤很難做,不少群友受傷嚴重,才得知原來不止我本人受傷嚴重,大家都多多少少受傷了,我也比較釋懷了。
「有些事就是要做了才能知道是做錯了」,沒有去做,就沒辦法有能力先知先覺,預判是錯的。
交易這條路就是這樣啊!
誰能在錯誤中尋找正確的出路才能勝出!
與大家共勉!!

為什麼我這麼推崇【亞當理論】? 寫給想進入交易的你。

亞當理論,是一本很薄的書。 前面2/3 都在講同一件事。

順勢而為。

各種換句話說,各種舉例,通常看完的人會沒有感覺, 然後書中的後1/3 開始講亞當理論提到的第二映像圖, 書中以棉花的日線當舉例。

很多人也是看看,就過了。 殊不知,精華,都在這了。

這本書開宗明義跟你說,別預測市場, 然後後半段又跟你說,可以用亞當來預測, 這不是很矛盾嗎?

一點都不。

他反而是在告訴你,如何順著主趨勢做交易, 在主趨勢回檔時,建立好的部位,甚至,加碼。

我剛開始接觸亞當理論時,我賠的稀哩嘩啦。 你所熟知的技術分析,我全部都會。

一度,我還憑空寫的出KD指標的公式。 但有甚麼用? 還不是一路賠一路賠.....

甚麼時候我開始穩定下來? 從我開始相信【簡單】就能賺錢開始。

我所教出來的東西,一點都沒有特別的, 也沒有甚麼繁瑣的公式或指標, 只有簡簡單單的壓力支撐。 簡單的說,就是K棒,以及型態。

價格才是最真實的走勢,價格往上代表有人買, 但,是否有人持續買,就要看價格回檔時,有沒有出現跌不下去的跡象, 這才是我們觀察的重點。

價格創高或破底的原因是甚麼,說真的一點都不重要。 重要的是,你做了甚麼事? 有沒有相對應的SOP?

有人說我的東西太簡單,人家學會了,就不會來找我了。

以教學來說,這不是很好嗎?

真正困難的,是心,不是市場。

所有想要找我學的同學們,有三本必買的書,都是超棒的。

  1. 亞當理論
  2. 幽靈的禮物
  3. 紀律的交易者

先看,再來找我上課喔。

為什麼砍掉虧損這麼難? 從心理學出發!

本週沒有交易夜,我要去看G4的棒球, 但我也不會忘了大家,我們來討論為什麼砍掉虧損這麼難?

先從心理學來講好了, 人的心理上會自然而然記得不好的事情, 尤其是對自己有重大生命危險或傷害的事情,

你記得你騎車順順安全的時候嗎? 不記得吧? 但是你卻可以立刻講出, 上次你在某某路口,差點被某個不長眼的三寶撞到, 某個路邊的車子突然把車門打開來,好險!

身邊的人呢? 他的好你可能還要想一下, 他上次做了甚麼讓你傷心的事情卻記了3年。

為什麼?

我們是動物, 動物為了生存, 我們會本能地把注意力放在避免風險上, 上次這邊有危險, 我們就會把這個【感覺】放入最深層的海馬迴, 放入潛意識內。 畢竟,活下去才是自然界最重要的事情。

下次,有這個【感覺】的時候, 就會啟動逃跑機制,先跑為妙,不要面對。 這就是心理學著名的Fight or Flight(戰鬥or逃跑) 反應。

那,這件事跟停損有甚麼關係?

有,關係大了。

因為我們都把戰鬥與逃跑用在錯的地方了。

跟行情【戰鬥】到最後一分一秒, 遇到虧損選擇【逃跑】不要面對。 這根本找死! 真正的作法是, 【戰鬥】用在自己身上。 【逃跑】用在行情上。

在人生上,有骨氣、有鬥志、永不放棄是超棒的心態! 在人生上,你要成功,真的要堅持下去, 不要被閒言閒語給左右。

但,放在交易上, 就只會出現不到黃河心不死的勇者... 然後,然後,然後黃河就到了...

【戰鬥】不是用在交易上。 我們並不是在跟市場戰鬥, 你不可能可以掌握行情走勢, 過去不行,現在不行,未來更不可能。 永遠不可能。 反而我們只是觀察出一些跡象,順著走而已。 在交易上,請當很懦弱的懦夫。 反而是在面對虧損時, 面對自己訂的計畫,計畫中的虧損來的時候, 要拿出【戰鬥】的勇氣, 面對自己,拚了,砍虧損! 面對波段單,拿出【戰鬥】的勇氣, 看著獲利回吐,堅持住原定計畫。 (不賠,其實滿好堅持的,我也不想測試我的人性 哈)

而【逃跑】呢? 逃跑可以讓我們選擇不要當下面對。 試想,本來明天要交的報告, 老闆突然說下週才要交, 你是不是鬆了一口氣,走,唱歌去?

這是人性。 可以不用今天面對的,就明天面對。 可以下週再面對,就下週在面對。

虧損,是很沒面子的對吧? 尤其是當你昨天才跟人家講你做了甚麼交易,多空, 結果今天反向一根, 很難交代對吧? 這時候,逃跑反應就出來。 逃避,找理由圓謊,找理由說服自己持倉, 找理由讓自己在人家面前有面子。 因為,面對虧損的【感覺】不好。 所以,不要有這種感覺。

但這時候,我們人性上卻不是砍掉虧損, 因為只要單子還在,你永遠就還有扳回一城的一線希望。 一出場,虧損就確定了,就真的虧了。 所以人性上,那個【希望】, 希望會回去的希望,希望讓自己有面子, 希望這波賺錢帶媽媽出去玩, 沒有賺怎麼帶家人出去玩呢? 這個【希望】, 讓我們選擇【逃跑】,不去面對立即虧損, 禱告明天。 但,市場不是抱著希望就會有用。

正確的做法是甚麼? 其實【逃跑】是對的, 但【逃跑】不能只有心理逃跑, 你真正的部位也要執行下去,才是真正的逃跑。

有賠過大錢的人更可以體會, 小賠時不逃跑,大賠時跑不掉。 明知要跑啊,但就跑不掉。

再講一次, 【戰鬥】用在自己身上。 【逃跑】用在行情上。

以上講這些心理的東西, 如果你沒有一套標準做法, 你也根本無法知道是方法的問題還是心理的問題, 五日線被雙巴就換十日線, RSI鈍化就換均線, 你到底有沒有一個標準SOP?


《股市大贏家:我用K線寫日記》-重點整理與心得

正確的交易者心態

@ 對於股市,我知道得越多,卻發現自己不懂的地方更多。(P.30)

@ 贏家集中力量;輸家分散力量。(P.36)

@ 做股票讓我體會到古希臘哲學家柏拉圖所說「生活必須像遊戲」,也帶給我恍然大悟的驚奇。(P.38)

@ 我們要在專業的領域上找機會,而不是撈過界或撈一票就轉檯。(P.46)

@ 玩股票就像做復健最忌躁進,但一般人常低估了影響股市的種種變數,厭煩於研判後市時的種種但書,渴盼一個能提供直接了當的答案並永遠適用的一夜致富術。當初學者問道:「如何做股票?」這通常意味著:「如何輕鬆快速地賺大錢?」(P.50)

@ 在股市,急於一飛沖天的人常四處碰壁,而循序漸進的人方向卻越走越清晰。即使有人能一夕致富,背後卻潛藏更大的一夕至貧的危機。因為一下子變得太美好,常讓人消受不了,加上錢來得太快,常讓人把錢看輕。為了重溫勝利的滋味,他將草率地再度進場,而且更勇於投機,但僥倖只是一時,他終將連本帶利還給市場。(P.52)

@ 投資人一開始的態度就已經決定了他們日後的勝負。(P.53)

@ 對心態正確的投資人而言,他們找到適合自己的舞台,然後全力以赴;他們不會妄想一上路就登上高峰,無懼重重險阻,對股市的熱枕,讓他們從跌跌撞撞中走出自己的路。(P.53)

@ 輸家不敢贏、不認輸,所以賺小錢、賠大錢;贏家拿得起、放得下,所以賺大錢、賠小錢。(P.55)

@ 如果我們把股票當事業,願意用一輩子的時間來探索,把事情想得長遠些,拉大格局,就比較不會在意一時的得失,畢竟每一回合的競技,都只是我們成千上萬次比賽的其中一場。(P.56)

@ 由於追求自尊的偏好和害怕後悔的心理,一般人大多有「損失規避」的心態,想的是如何增加贏的次數,而不是提升贏的品質,傾向先賣掉賺錢的股票,逃避了賺錢的風險,而接受了賠錢的風險。也就是說,一般人不敢賺大錢,所以只能賺小錢;不想賠小錢,結果卻賠了大錢。(P.58)

@ 所謂:「眼界決定境界,定位決定地位。」只想要在股市謀取生計或只想賺點錢貼補家用的人,注定要陪公子讀書,成就一定有限。我想,如果一個人連對自己的未來都缺來想像力,對股價怎麼會有想像力呢!反觀,那些替自己設立很高標準,大膽挑戰自己極限的人,雖不一定會達成目標,但總有一定的成就。(P.75)

@ 輸家只想做「保險的」選擇;贏家不認為有什麼選擇是「保險的」。(P.78)

@ 迷思:曾經,我在股市頻繁進出,我用各種藉口合理化自己的績效不佳。

解答:後來,我發現問題出在自己缺乏一套做股票的核心技術,以致看問題時只看到表現和眼前,看不到事情的內涵和展望。(P.86)

@ 贏家進場前先找出路;輸家憑感覺殺出血路。(P.87)

@ 輸家認為跟在別人後面才安全;贏家發現後面跟了太多人而不安。(P.107)

@ 一般人大都不喜歡冒險,對於所費不貲的新事物,例如新發表的車款,往往要等口碑建立了才敢嘗試,但是新車在一段時間內不會改款,隨時可以買到一模一樣的車子,股價卻是隨時變動而不等人的。(P.118)

@ 在日常生活中頗有幫助的心理因素,只有在處理靜態情況時才有用,用在起伏不安的股票世界,反而讓人迷失。(P.119)

@ 贏家導因不導果;輸家愛放馬後炮。(P.132)

@ 贏家織網捕魚;輸家混水摸魚。(P.142)

@ 輸家不是把做股票看得太複雜,就是看得太簡單;贏家把交易的複雜性條理化,把做股票變簡單。(P.178)

@ 大贏家原本只是個精益求精的小贏家;大輸家原本只是個一錯再錯的小輸家。(P.203)

@ 輸家喜歡別人給結論;贏家扮演自己的顧問。(P.228)

@ 單飛是股票族實現命運的必需之路,不追求歸屬是最好的歸屬。我們要剪斷與他人之間的臍帶,經過了市場的大風大浪,才能測試出適合自己的版本,然後展翼高飛,飛到一個連自己都大吃一驚的境界。(P.253)

@ 贏家看盤時忙著比較個股間最新的相對數據;輸家看盤時只關心股價的漲跌。(P.254)

@ 贏家揣摩操作原則,愈磨愈利;輸家任憑感覺出招,時好時壞。(P.307)

操作原則與精神

@ 我們要先順從市場,才能貼近市場,貼近了市場,市場自然而然會告訴你問題出在哪裡,又該如何修正。(P.25)

@ 我們的想法或行為,大致上可分為建設性的和消費性的。一般人具有較多的狐狸性格,他們沒有找到自己覺得重要的價值觀,只想得到短期的好處,對很多事情感興趣,但這些想法或行動缺乏一致的願景,甚至互相矛盾,以致這些原本是建設性的想法或行為,不能匯聚成一股力量。甚至,他們會自動自發做的事,往往都是討好自己的消費性行為或一些雞毛蒜皮的事,無關人生願景。

反觀,刺蝟型的人單純、專注而且堅韌,對他們而言,「找到重要的價值觀」並不是說一定是要立志做大事,而是要摸索出對自己有意義又適合自己的舞台,把自己最好的一面呈現出來。他們知道自己的時間和資源就那麼多,所以不會浪費在無謂的消費行為上,而義無反顧地投資在具有建設性的事務上;他們效法某些商號的負責人經營百年老店的心態,堅守住自己最了解的領域,逐步累積競爭力。(P.41)

@ 很多人認為股市沒行情時,在裡頭耗了好幾個月,賺的錢還不如行情好時一天賺的。但如果不長期駐守在股市,怎麼抓得住行情好的那一天?(P.47)

@ 不僅是做股票的態度要專注,我們還必須從積極實戰中測試出一套適合自己的系統化方法,然後改良精進,而不是不停地更換方法,否則就無法累積經驗,而像「神經語言程式學」的訓練師查理·福克斯所說的,「有了六次的一年經驗,而不是有了六年的經驗。」(P.48)

@ 巨大的財富不是從一進場就開始「一瞑大一寸」來的,而是等我們的基本功做好了,能量蓄足了,財富才突然暴增。就像股票打底打得夠久,突破盤整後,常上演噴出行情。(P.51)

@ 減輕心理壓力的三大心法-不要太在乎輸贏;挫折越大、離成功越近;格局要寬廣。(P.60)

@ 成本觀念是金融操作上最大的迷思,它不但是投資人先入為主的一種定見,也涉及了投資人害怕後悔的心理。(P.116)

@ 我覺得「先有手感,才有靈感」,操練的次數夠多,觀察力自然越來越敏銳,就會漸入佳境,甚至會形成「洞見」。「洞見」不是突然頓悟的,也不是上天賜予的,而是千錘百鍊來的。一般人以為「千年暗室,一燈即明」,只看到別人大放光芒的那一刻,殊不知這些人在發光發熱之前,暗地裡已經操練了無數次。(P.126)

@ 人是情緒的動物,所以情緒會戰勝邏輯;人也是習慣的動物,所以先入為主的定見和日常生活中的邏輯,又會戰勝股票市場的邏輯。因此,除非我們找到一套自己可以信服的系統性方法,並訓練有素,否則我們做決策時,即使不被情緒所左右,也常陷入先入為主的定見,或被日常生活的邏輯所擺布。(P.126)

@ 我操作上的第一個大原則是「未來式原則」,就是「後市看漲還是看跌?」股價漲了很多或股價看來很高,不該是賣股票或不買進的理由,某支股票賣掉後價位拉高,也不該是不追價的理由,某支股票賣掉後有了差價,也不該是回補的理由;此外,某支股票過去的表現如何、某支股票現在讓我賺了多少或賠了多少、哪些股票曾讓我嘗到甜頭或吃足苦頭,全都無關決策。(P.127)

@ 我操作上的第二個大原則是「比較原則」,就是「哪支股票比較會漲(跌)?」個股經過比較就有優劣,有優劣就可以取捨,只要能找到更會漲的標的,我就不必介意股票總是買得太晚、賣得太早,買賣股票也變得不再棘手,懂得放手讓我得到更多。

我用機會成本的觀念來取代成本觀念。我不必耿耿於懷於錯過的價位,不必等待該買(賣)而未買(賣)的價位重現。我綜合評估股價漲跌空間和完成漲跌空間的機率,作為汰弱換強的依據。(P.128)

@ 有時候手中持股的基本面、技術面俱佳,我也會加碼,並擇善固執地逮到全壘打,體驗了「全壘打是安打的延伸」。(P.135)

@ 方法正確遠比結果正確來得重要。(P.138)

@ 市場是個嚴厲的老師,剛開始總讓我們嘗盡苦頭,等我們慢慢摸熟它的「脾氣」,它還是會給我們機會的。(P.139)

@ 一般人採用能讓自己心安的方法,或根據經驗法則來操作,只敢買績優股、低價股、股價淨值比低的股票或低本益比的股票,並遵循「不要追高殺低」、「買黑賣紅」或「逢高減碼」的原則,也因為這份安全感付出了代價。正如威廉·歐尼爾所說,「股市最矛盾的狀況之一就是,看起來已經很高的股價,往往還會飆得更高;看起來很低的股價,經常會跌得更低。」(P.187)

@ 一般人只追求不輸錢,只想做穩當的交易,他們採取守勢,而不願意處心積慮賺取大利潤;他們追求較高的成功率,而不是追求較高的期望值。另一方面,因為市場大部分時間處於區間盤整,高出低進的成功率較高,由於慣性使然,他們在多頭市場不敢追高,在空頭市場也不敢殺低。但更糟的是,在相對低檔不敢追高的投資人,常常會因為股價在相對高檔整理了一段時間,習慣了高股價而跑去追高,而且不知道何時開溜。同樣的道理,他們會在相對低檔賤賣持股。(P.188)

@ 如果投資人不能堅守以技術訊號為依據的單一交易系統,隨著消息面或自己的感覺起舞,就很難印證技術分析是不是管用。即使賺了錢,但賺錢的來源是隨機的龐雜訊息,很難如法泡製,這不是有效學習。這種不扎實的勝利,如果只是比賽幾回合,可能勝負互見,但如果戰線拉長,注定要一敗塗地。(P.194)

@ 我知道我應該遵守操作原則,把握我所能把握的,不必因市場不能掌握的因素所造成的失敗而懊惱。(P.202)

@ 對於股市新手,如果一開始就喜歡操作,習以為常後,自然會積極地操作;但對於不喜歡換股操作、長期績效又不彰的投資人,要改弦易轍較為困難,因為積習難改。我覺得後者不妨嘗試把自己歸零,假裝自己是股市新鮮人,而且喜歡操作,撥出一部分資金來練習,規定自己一周至少要進出幾次。剛開始不要把焦點放在盈虧,而要從操作中找出自己的不足並補強,這就是「給思想設計了任務」。(P.206)

@ 我訓練自己看事情時能置身事外,而以市場的角度來揣摩市場的反應,即使不能正確預知市場的反應,也要視市場的反應來修正策略。(P.213)

@ 我們要在股市追求自由,就不能被市場上的成規和自己的定見所束縛,而失去了摸索和選擇的自由。即使是市場裡司空見慣的操作原則,我一定要經過實戰的反覆驗證,才把它納入自己的準則。(P.214)

@ 我們要有正確的心態來引發學習並持續學習,藉由不斷的實戰和思考,摸索出適合自己的科學方法;隨著時間的累積,當經驗的資料庫量越來越多、質越來越精,我們操作時更能旁徵博引,對行情做出系統化的解釋,方法也越修正越能以簡御繁,碰到大部分的市況,都可以從容以對。(P.224)

@ 在股市,答案往往不只一個,今天看來是對的決策,過些時間可能是錯的,何況股票的漲跌有時候無法以常理解釋,因此沒有人可以提供標準答案。做股票成功的基礎在於隨機應變,培養自己獨立思考的能耐才是最上策。(P.235)

@ 傑西·李佛摩建議投資人隨身攜帶一本小冊子,第一頁寫著:「提防內線消息-所有的內線消息。」剛開始我感到疑惑:內線消息又不是毒蛇猛獸,為什麼要避之唯恐不及?我終於明白了,或許他認為所謂的內線消息是投資人成為優秀操盤手的最大阻礙,因為這些隨機的消息,往往會干擾我們的操作模式,甚至喧賓奪主地取代了我們原先使用的科學方法,導致我們和市場之間的聯繫方式變得不正常。(P.246)

@ 當我們看一本書時,如果我們對其內容感到豁然開朗,表示書上介紹的領域是我們不拿手的,我們切勿在該領域輕舉妄動;如果我們對書上的內容能夠心領神會,表示我們對這個領域有一定了解;如果我們可以對這本書的內容加以批判,並融進自己的創見,那表示我們是箇中高手了。(P.248)

@ 我們真正要學習的是這些大師共同的精神和行動力,如果要參考他們的實務經驗,只能把適合自己的部分拿來用,而不是被他們的方法所綑綁,一旦融入了自己的元素,這樣的操作模式才有生命力,因為做決策的人是自己。(P.249)

@ 如果說金融操作者在順境時首重爆發力,那逆境中就首重堅韌性。賠錢時,我們不必在乎別人的評價,才能困而知之,突破本身的極限,就像做瑜珈,撐過了最痛苦的時刻,才能突破學習的瓶頸。(P.256)

@ 股市中當然沒有絕對肯定的事,但我們總可以採取一個勝算相對較高的方案,也不必等到事情明朗了才做;除非評估的結果是「不做」比「做」好,否則我們就要採取行動。(P.282)

@ 當情勢對我們極其不利時,我們常讓情緒凌駕判斷,自欺欺人地安慰自己「不賣就不賠」,或虛張聲勢地說,「等跌下去再加碼攤平」,就像賭徒賠錢時,堅持留在場內,幻想著馬上會拿到足以翻本的好牌,以致越賠越多。(P.283)

@ 應該大賺時,少賺就是賠;應該大賠時,少賠就是賺。(P.286)

@ 一個投資人如果不談利潤,難免有點虛假,但如果開口閉口都是錢,也成不了大氣候。太在乎錢的人,只能賺一把是一把,常只看到眼前利益,看不到大的方向,不會投資時間去建立一套長期可以適用的方法;太在乎金錢的人太想贏,難免會綁手綁腳,他們賠錢時度日如牢,但賺錢時也未必好過,因為股市的錢永遠賺不完,他們常常會有「這裡少賺,那裡也少賺」的後悔,因此亂了陣腳。(P.310)

@ 熱愛交易的人操作時融入遊戲當中,好奇心凌駕於得失心,好奇心驅使他們試著找出股價的規律性,在大起大落時,多了一點超然物外的心情。比如,對於手上一支能創新高的股票,我抱著「我要看看它會漲到哪裡」的好奇心,這樣比較抓得住波段行情;對於手上沒有的、續創新低的股票,我抱著「我要看看它會跌到哪裡」的好奇心,這樣才不會過早搶反彈。(P.312)

@ 我發現交易與心理實為一體的兩面,先了解自己才能了解交易。(P.314)

@ 我認為要看透大盤趨勢較難,但要看清個股相對強弱顯然容易多了,因為個股經過比較就有優劣。只要選股選得好,大盤走多時可以多賺,誤判大盤走勢時,也會少賠。調整持股,除了讓我獲得更高的績效,也像身體的律動,讓我心裡更加平衡。(P.316)

@ 一般人建立操作原則時最大的錯誤,就是浪費太多時間,想要找出起漲點和反轉點,以便能夠買到最低價、賣到最高價,一旦不能在底部或頭部一次搞定,失之交臂的價位常會盤據心頭,因而亂了步調。(P.319)

@ 為了不會過早搶反彈(或錯失波段行情),我寧可等市場自行決定了底部(或頭部)後,再採取應對措施,要不然就是藉由換股操作,抓住個股漲勢中間容易操作的部分。(P.319)

投資與投機

@ 任何一筆買賣,關注的不外乎價值和價格。投資比較偏重於價值,投機則比較偏重於價格。但價值和價格並非壁壘分明,它們之間的關係就像主人遛狗,狗反覆地在主人前後奔跑,離開太遠時就會折回主人旁邊,主人就是價值,狗就是價格,價值和價格之間有一條看不見的繩子。因此,即使是投機客,也不能像王爾歌德劇中的名句所說,「對價格無所不知,但對價值一無所知。」(P.94)

@ 儘管有時候投資和投機比鄰而居,但我們還是要先釐清自己要投資或投機,才不致模糊了選股的焦點,操作上進退失據,以致兩頭落空。我認為短線投機的最大悲哀,就是因為被套牢而淪為長期投資。(P.95)

@ 長期投資者主要是買進績優股、買進低估股和成長股,然後長期規劃;而投機客則買進短期內可能有價差利潤的股票。換言之,投資是傾向買進好公司的股票,而投機是傾向買進即將上漲或還會再漲的好股票。

然而,好公司的股票可能是不動如山的爛股票,而爛公司的股票可能是即將一飛沖天的好股票,因此有別於投資家以經營者的角度看問題,投機客重視股票的股性,遠甚於公司經營的業務性質。(P.97)

@ 投機的「機」字,意味著新機會,不只是契機,也包括危機。投機客善於判讀市場,預測或關注新情勢、新趨勢、新技術、新應用、新產品、新管理階層、重大性併案,從轉變中創造價值。(P.99)

@ 打擊者最大的機會是逮到對方投手失投的「甜」球,股市投機客的最大機會則是逮到大部分投資人都已失去理性的行情。(P.187)

技術面觀點

@ 我覺得看盤有點像看戲,往往一開盤就高潮迭起,如果事先未能融入情境,常會看得眼花撩亂。(P.44)

@ 如果我們缺乏中線的方向感,短線即使看對,也不敢下大注,也可能因股價的跳空而無法回補(賣出)股票。(P.105)

@ 我發現泡沫行情總是一再重演,但在泡沫剛開始形成時,總有一些泛泛之論提醒我們市場已過熱。為了怕泡沫破滅,因而錯過了泡沫破滅前波瀾壯闊的大行情,形容因噎廢食。泡沫行情意味著股價將跳脫基本面的束縛,因此以基本分析預測股價波動是不夠的,此時根植於空中樓閣理論,用以推敲市場其他參與者心理的技術分析,將適時提供買賣點,幫我們克服面對泡沫行情的恐懼和貪婪。(P.149)

@ 就像寫作時,我們用具象的詞彙來形容抽象的事物,好讓想像有所依據,為了把抽象的群眾心理具體化,我以符合「簡單具體」原則的技術指標做決策的依據,這不但能克服心理上的偏誤,提升決策的品質,還能提供決策速度,這麼做也符合人性中服從一套秩序規範的渴望。

就像寫作時,我們還必須用抽象的詞彙來形容具象的事物,以增加想像空間;同樣地,做股票時,光靠量化的基本面和技術面資訊還不夠。(P.153)

@ 技術分析不強求是什麼原因造成股價的波動,而要借助歷史總是重演,由辨識目前走勢的型態,推斷未來股價「將如何」。(P.163)

@ 基本分析具有以下三個瑕疵:一、資訊和分析可能不正確。二、股價的合理價值很難認定。三、合理價值和實際價格的差異,事實上隨時隨地都存在。(P.175)

@ 真正教會我技術分析的是股市。我把課堂上教過的指標及其應用方法,逐一套用在過去的走勢圖,驚訝的發現,單一指標的成功率常只有五、六成,這跟「用猜的」又有什麼兩樣?其實不然,技術指標一旦集體運作,準確率就會大為提升。(P.180)

@ 短、中線技術指標各有所用-每個項目的實用性視當時型態而有所不同,例如箱型整理時,很難以移動平均線做進出依據,日KD相對靈驗;此外,關鍵價位的功能也經常互換,原來的線形支撐,一旦有效跌破,反而成為後市的壓力。(P.183)

@ 事實上,如果市場大幅拉回,讓我們有便宜貨好撿,這可能意味走勢沒有原先想像的強。(P.189)

@ 我堅信「股價創新高必有重大意義」,創新高的股票就像發射台上點火待發的火箭。事實上,讓我獲利最多的交易,不是買進那些價格在底部附近的股票,而是買進能創新高價的股票。(P.190)

@ 事實上,中線多空反轉與否,季線是重要的分水嶺,尤其當大盤弱勢跌破季線,而且季線正要走平下彎,盤勢更是岌岌可危。(P.201)

@ 我選用的技術指標在精不在多,以免造成混淆,選用的重點在於指標的組合必須兼顧不同的面相,才能預測出趨勢和強弱勢。我同時審視這些常用指標的多空意涵,進行系統性的思考,給予價值判斷,有時會憑藉關鍵的少數指標來做決策,而不是少數服從多數。(P.213)

@ 我看盤時的三大重點:一是成交量,二是哪些股票即將漲(跌)停,三是那些股價將突破近期或歷史高(低)價。(P.263)

@ 當冷門股量能持續溫和放大而且價漲,表示需求已經顯現,常是大漲的前兆。我注意哪些股票沉寂已久,成交量開始活絡?哪些股票量大增價不漲,已是欲振乏力?當個股漲(跌)停板鎖住或創新高(低),常是漲(跌)勢延伸的訊號,尤其是久盤後出現的第一根漲(跌)停或突破。(P.264)

@ 大盤跌深或久盤後出現長紅,我習慣搶最早漲停的個股;大盤漲多或久盤後出現長黑,我總是先賣出最先觸及跌停的個股。搶進時,原則上越早漲停的個股越好,如果個股漲停同時領先突破近期高點,更是上上之選。但如果該股短線已經急漲,就必須斟酌。(P.266)

@ 多頭初試啼聲時,我買盤面最強勢的股票;漲勢確立或漲勢再起時,我買進領先突破近期新高的個股;多頭拉回時,我選擇抗跌性較強的股票。此外,當行情持續大漲,我也會注意正要轉強的落後股。(P.268)

@ 個股能否有效創新高,始終是個難題,假突破的數目可能比有效突破的數目還要多!藉由看盤的細部分解,讓我的技術分析如魚得水,例如某支股票雖逢前波高點的反壓,但當大盤拉回跌破當日低點,它卻未跟著大盤跌破當日的低點,甚至逆勢演出,則後市值得期待。我喜歡買進盤面強勢而且線形也相對強勢的股票,尤其是在利空中相對抗跌,甚至持續上漲的股票。(P.269)

@ 除了技術面的警訊外,我體驗到漲勢已達強弩之末的可靠訊號之一,就是盤面的焦點集中在少數所謂「強者恆強」的股票上,就像光靠極少數明星球員的職棒隊,要登頂的難度很高。

此外,當漲升股票的內涵不佳,表示資金後繼乏力,也是反轉的警訊。(P.270)

〔心得〕

之前做過《股市大贏家II》的重點整理與心得,現在回頭過寫第一集。本書的作者為短線技術面玩家,而個人雖然不是短線玩家,但巧之又巧的是我們都使用了比較原則,把比較原則納入自己的系統裡,在其他書中我從來沒看過別人寫過這種方法,要不是看了本書,我還以為這個方法和名詞是我原創的XD。我個人把成功的交易者分為兩個類型:將軍型與軍師型,將軍型的人衝鋒陷陣、見機行事,軍師型的人居高佈陣、謀定後動,個人屬於軍師型,而本書的作者屬於將軍型。由於將軍型做中錯、錯中學的特質,所以不少將軍型的高手都是先破產後才絕境重生,本書的作者也同樣經歷過破產,也同樣的大破大立後成為股市贏家。以書的封面12年從5000萬到10億來計算,平均年複利報酬率39.5%,離個人的高手標準50%還差一點,但資料範圍如果涵蓋5000萬之前的數據,我相信超過50%的可能性很大。這也是將軍型玩家的另一個特性-要嘛失敗、要嘛成為高手,當然我們可想而知能成為高手的實為少數。因為作者本身如此的經歷,讓他深刻體會到人性在玩股票上的重大弱點,所以本書很細心的給了非常多的心理路標,讓我們可以藉由了解自己而超越自己。也因為如此,本篇我把非常多的段落都節錄下來,也許有些指著相同的方向,但不同的人可能適合不同的路標,所以讀者若能仔細品嘗每一項重點(當然直接看書效果更好),你就能慢慢體會成功交易者是如何看待市場,而藉此讓自己踏出正確的一步,因為就如本篇開頭的文字所述-"投資人一開始的態度就已經決定了他們日後的勝負"。

《股市大贏家Ⅱ:贏在修正不在預測》- 重點整理與心得

贏家與輸家的差別

@ 輸家很難從錯失勝利的打擊中恢復;贏家從錯失勝利的打擊中脫胎換骨。(P.28)

@ 輸家檢討別人,尋找藉口;贏家檢討自己,尋找出口。(P.40)

@ 輸家用基本面保護技術面,卻總在股價大跌後才知道基本面已經變壞;贏家從股價走勢所透露的訊息中,尋找股價大跌前的警訊。(P.123)

@ 輸家為了符合自己的感受,有時以基本分析為主,有時以技術分析為主;贏家在技術分析和基本分析之間選一個為主,並全程堅持以這一個為主。(P.149)

@ 輸家以為用愈多條均線,愈有保障;贏家知道只用三、四條均線,才是直截了當。(P.183)

@ 輸家掛念已經過去的,期盼還沒發生的;贏家順應每一個過程,把握當下做修正。(P.199)

@ 一般人做股票時經常顯得猶豫的一個原因是,讓招式和招式之間互相牽制。例如,想要兼顧基本面和技術面,但兩者卻互相牴觸;或是想要兼顧短中線和長線,但兩者卻經常分歧。(P.231)

@ 輸家錯失最佳機會後,無心去抓次佳機會;贏家認為大致的正確好過追求不到的完美。(P.252)

@ 輸家在行情翻空時總有各式各樣不賣股票的理由,所以長期住套房;贏家做股票像住旅店,該退房時不留戀。(P.330)

認知股市的無常

@ 做股票之所以好玩,因為交易者總是活在懸念中。(P.22)

@ 我們在生活中碰到的事即使會改變,也常是漸進式的改變,就像季節的變化總是循序漸進,因此,只要觀察一陣子或是聽別人經驗傳承,我們就足以應付。但股市的變化有時是跳躍式的,而且只占少部分時間的大起大落,卻決定了我們的成敗,所以我們需要花很長的時間,透過大量的實戰經驗來做歸納,還要對歸納出來的結果出現意外的可能性,抱持高度的警覺;也就是說,參考經驗,卻不過度信任經驗。這樣即使不能每次都穩操勝券,也有一定的穩定性。(P.35)

@ 我發現不但賠錢的事要慢慢適應,連賺錢的事也要慢慢適應。根據我的觀察,賺錢時愈亢奮的人,賠錢時愈沮喪,這些情緒容易在兩個極端之間擺盪的人,常把自己搞得筋疲力盡,風險承受能力差,通常功力平平;而高手一定都是從容的,愈是高手,愈不容易在剛收盤後從他講話的聲調中聽出,他剛才是大賺還是大賠?

在股市,我們愈想賺錢,很有可能股票一有利潤就把它賣掉,反而賺不到大錢;愈怕賠錢,很可能股票一虧損就不賣,反而賠了大錢。愈不在乎輸贏的人,愈不會讓恐懼和希望等情緒主導進出,而是由自己對後市的看法來決定買賣,愈能賺錢。(P.37)

@ 事事會過去,而「賠錢」也是一種淬鍊。(P.39)

@ 我們的「知道」是事後才知道,不是事前就知道。我們在事後都準確的知道某支股票應該如何地買低賣高或一路長抱,會很正常的反問自己當時為什麼不堅決地這麼做。「後見之明」最大的問題是讓我們高估了自己的預測能力。(P.46)

@ 投資人尤其要經過大場面的洗禮,穩定性才會增加。當我們使出渾身解數和一次大波動行情周旋,即使覺得力不從心,從中得到的成長,比從一百次小波動中得到的還多很多。見慣了大場面,對市場上發生的一切都不會感覺太吃驚,比較能適應股價一下子跳到極端。(P.63)

見招拆招、不死等解套、汰弱留強

@ 我們把命運交給別人、市場、公司派和政府,讓小危機坐大成大危機,常常還沒等到股價出現像樣的反彈,就面臨融資追繳。如果我們被套住後,只會等著解套,最大的問題不是不見得能等到解套,而是就算等到了,也虛度了很多光陰,浪費了很多機會成本。(P.55) @ 做股票要成功,不見得要預測得很準,而是要修正得很即時;如果我們善於做修正,在做預測時會更有恃無恐,而比較敢出手。(P.72)

@ 不同的個股一經比較就有優劣,有了優劣,我就知道如何取捨。懂得放手讓我得到更多,我用比較的觀點,豁然開朗地看到了一片新天地。(P.108)

@ 不要說當年,即使在今天,我也抓不準某檔股票在未來一個月內的相對高價,我擅長的是在漲勢或盤整中,對自己的持股不停地汰弱換強。我也體會到股價的莫測高深,只有在頭部或底部出現一段時間後,我才有後見之明。不過,這已足夠讓我在市場上賺錢。我的優勢在於我可以廣泛地比較每一支股票的消長,用一套慣用的訊號來招呼自己進出場,而不是買來買去就那幾支老面孔,正如李佛摩所說:「我操作一種系統,而不是操作一支喜愛的股票。」(P.115)

@ 時間就是風險,也是成本,我覺得做股票時,抓住漲勢確立的訊號,比抓住底部更容易也更有意義。買進股票的時機比價位更重要,往往付出的成本要更高,才更確立股價要漲了,我要賺的是股價最會漲的那一段,而不是看股價便宜就買起來等。(P.119)

@ 我們買股票就是要賣,但「有賺錢」不該是賣股票的理由。我賣在個股漲不上去或要下跌了,或資金有更好的用途時;我重視賺錢的效率,奉行「漲久不如漲快」。在多頭市場,我重視的不是某支股票「總共讓我賺多少錢」,而是「在多久的持股期間內,帶給我多少的報酬率」。如果我能持續抓到會漲得更快的股票,雖然我只賺到過手的股票中漲幅的一段,但把每一段利潤加起來,相當於我持有的股票漲得快又漲得久。(P.120)

@ 大盤剛反彈時的強勢股往往不是趨勢翻多後的主流股。(P.290)

技術分析建立在人性

@ 技術分析用來抓市場心理,它可以用來預測股價的基礎,是假設歷史會重演,這是因為即使市場上已經換了另一批人在玩,但人性始終不變。例如,指數在大漲過後跌破60日線,代表這60個交易日來買進的人從賺錢到賠錢,投資人開始不安,這是多頭行情反轉的警訊,這時就該減碼。(P.101)

@ 由於預期心理,股票有別於一般消費品「價格上漲會抑制買氣」的特性,經常愈能漲的股票,只因為投資人預期還會漲更高,愈有人追捧,價格可能只因為價格已上漲而上漲,和總體經濟或上市公司的營運無關;同樣地,價格可能只因為價格已下跌而下跌。(P.131)

技術分析的應用與優勢

@ 我們分不清楚自己要做短中線投機,還是要長線投資。通常,投資的週期比投機長,但一般投資人在操作時毫無策略可言,有賺就做短線,被套就做長線。(P.43)

@ 我認為長線要特別重視基本面,做短線要特別重視技術面。(P.43)

@ 例如,我們可以鎖定在低檔接連拉出兩支漲停的股票,在這種脫軌的行為背後,常常是由於有人知道一些內線消息,所以買盤才會顯得特別急切。(P.104)

@ 當大盤或個股沒傳出重大消息,而技術線行面臨攤牌時,時間不容許我對消息面追根究柢,我先做決策再說。(P.137)

@ 基本面的事比較零碎,技術面的事比較純粹,純粹才有力量。(P.152)

@ 股票的價值難定,而且基本面可能有我們不知道的重大變化,所以我們很難界定股價是否漲過頭或跌過頭;何況,超漲或超跌原本就是市場的常態,如果太重視基本面,就不能隨著市場的進展來應變。(P.153)

@ 愈是在關鍵時刻,過去所發生過的線形,重複發生的機率更是高得驚人。(P.219)

@ 強勢股包括當天盤勢中的強勢股和線形上的強勢股,我認為線形上的強勢股更重要。線形上的強勢股包括:漲幅較大的股票、領先向上突破同期高點或同期反壓(如六十日線)的股票,或大盤拉回時跌幅較小,甚至逆勢上漲的股票。(P.291)

@ 在看多加碼的過程中,如果我們的持股在之前的空頭市場幾乎未隨著大盤下跌或跌幅相對有限,我們常認為「抗跌就能抗漲」,因而不敢買進。但事實上這些抗跌的股票必有過人之處,一旦大盤轉強,它們很容易率先穿頭,引來追漲買盤。(P.297)

@ 直到學了技術分析,我才相信,除了極為罕見的突發大利空外,沒有「大跌前會不會有跡象」的問題,只有「在跡象出現時,你看不看得出來」的問題。(P.302)

@ 線形上的弱勢股包括:跌幅較大的股票、下降趨勢線的角度較陡的股票、率先跌破同期低點或同期支撐(如60日線)的股票,或大盤反彈時漲幅較小,甚至逆勢下跌的股票。(P.317)

@ 一旦某支個股的線形翻空,如果我們還執著於該股的本益比已經跌到十倍以下,或遷就於大家都已經知道的其他基本面的優異數據,雖然我們不賣,但因為空頭市場的接手很弱,只要有人殺出,股價照樣大跌。(P.317)

@ 當行情突然重挫,很多人不敢置信,紛紛問別人,「有什麼消息?」但有時候,突發的大跌是沒有理由的,何況,跌都跌了,要如何應對才重要。(P.329)

股票箱

@ 股票的走勢並非毫無章法,而是亂中有序,在運行過程中,形成了一定的價格區域-股票箱,股票箱的頂部是重要的壓力,股票箱的底部是重要的支撐,股價走勢是由一個一個的股票箱連結而形成的。(P.166)

@ 通常整理期間拉得太長,套牢籌碼愈積愈多,上檔反壓愈大,反轉的嫌疑就變大。(P.171)

@ 股價走勢的高點或低點之所以會形成壓力/支撐,因為這是交易群眾曾經失之交臂或食髓知味的價位,這些價位是市場參與者情緒的烙印,當市場再次給他們機會時,大多數人自然不會放過。因此,漲勢經常在近似的價位折返,跌勢經常在近似的價位反彈。(P.172)

@ 壓力和支撐的強度取決於下列因素。一、支撐區與壓力區的成交量愈大,支撐或壓力的強度愈強;二、交易區間的價格波動愈寬,所構築的壓力或支撐愈大;三、交易區間涵蓋的期間愈長或折返的次數愈多,理論上支撐區和套牢區的成交量愈大,支撐和壓力也愈強。(P.172)

@ 原來的壓力愈大,一經突破,所形成的支撐愈強。突破後經過的時間太久,隨著當時參與者陸續出場或記憶的遺忘,支撐或壓力的強度將愈來愈弱。(P.174)

@ 判斷真突破的原則-

一、任何型態的向上突破,通常要得到大成交量的確認,但向下突破時則不需要。

二、有效的突破,不管向上或向下,其技術指標會顯示相同方向的新高或是新低,如果價格和技術指標之間出現背離,則往往是假突破。

三、個股中線漲勢剛形成時的創新高,追高相對安全。

四、大盤中線趨勢往上時,個股向上有效突破的機會較大。

五、愈早領先大盤向上突破同期反壓的個股,有效突破的機會較大。

六、個股突破反壓的氣勢愈強勁,向上有效突破的機會愈大。例如,一開盤就跳空突破反壓,或突破反壓時上升角度變陡或指標剛轉強。 (P.174)

@ 向上突破後漲升空間不大的個股形同假突破。 (P.175)

@ 同樣是剛向上突破交易區間的個股,目前價位距重量級反壓愈遠、距反壓之間的累計套牢量愈小的個股,漲升空間較大,漲速也可能較快。線形上主要反壓包括前波高點(或低點)、中期或長期下降趨勢線、120日或240日均線等。(P.176)

最重要的均線-60日均線

@ 在指數下跌超過兩成並跌破六十日線後,反彈時通常漲到六十日線就會先壓回。...(略)...但如果拉回不深,就有可能是空頭力竭的徵兆。(P.17)

@ 在指數下跌超過兩成並跌破六十日線後,反彈時往往不會一次就站穩六十日線。 (P.18)

@ 技術分析主要是用來衡量短中期的市場心理,很難預測長期,因為誰能在今天就預見市場上好久以後的期待和恐懼,所以,代表大盤或個股中線趨勢的60日線就遠比代表長線趨勢的240日線重要。 (P.185)

@ 當股價在大漲後拉回而向下跌破60日均線,市場心理就從普遍獲利的慣性變成虧損,投資人開始不安,如果股價不能迅速站回60日均線,一旦該均線的角度轉而呈現下彎的趨勢,中線空頭走勢更加確認。同樣的,當股價在大跌後反彈而向上突破60日均線,市場心理從普遍虧損的慣性變成獲利,投資人持股意願增強,一旦60日均線的角度轉而上揚,宣告中線多頭走勢來臨。(P.186)

@ 60日均線的罩門-

*僅適用於趨勢明確的行情:配合60日線和股票箱來操作。當價格向上突破60日線,也向上突破交易區間時,更能確定漲勢形成。

*買賣訊號落後實際的反轉:配合K線的型態和其他原則來做進出。 (P.188)

@ 漲勢延續得愈久,一旦指數真正跌破60日線,代表近期內套在高檔的籌碼更多了,大跌的機率更高。同理,通常指數在大跌後反彈,並不會直接上攻,而是至少再殺一波,把投資人磨得受不了,以為股市沒希望了,然後才會大漲。 (P.228)

如何參考大盤

@ 當大盤跌深剛反彈時,同樣是即將漲停的股票,我傾向買之前跌最深而剛反彈的,而不是已有一段反彈幅度的;當大盤趨勢剛翻多時,我傾向買領先大盤翻多或領先大盤創波段新高的個股,而不是買趨勢落後的個股。當大盤逼近長期下降趨勢線,我不再積極換股,而是等著逐一賣掉手上露出頹勢的股票。(P.113)

@ 之前指數的漲幅愈大、套牢的籌碼愈多,下檔的跌幅可能愈深。順便一提的是,指數盤頭的時間愈久,通常表示套牢的籌碼愈多。(P.220)

@ 在指數大漲後,操作難度升高的背後,經常意味著大盤漲勢已經到了尾聲。(P.314)

@ 大盤處於相對高檔且為跌勢時,要超級保守;大盤處於相對低檔且為漲勢時,要超級樂觀。(P.346)

如何解析籌碼

@ MV=PQ- 我把其中的變數修正為,M代表流入個股的資金,V代表個股周轉率,P代表股價,Q代表該股流通籌碼的數量。方程式的左端代表需求,右邊代表供給。股價最易漲的情況是M和V增加,而Q下降。科斯托蘭尼的座右銘「股市的漲跌都取決於市場的傻瓜比股票多,還是股票比傻瓜多」,就點出同時影響股價的M和Q這兩項因素。(P.123)

@ 個股周轉率就是在單位時間內股票換手的次數。在某種程度上,股票周轉率升高,形同流入資金增加,有利於股票的上漲。(P.124)

@ 愈多人進場買股票,股票愈可能上漲,但如果有人源源不絕供應籌碼,等到每個潛在買盤都買了,股價就要反轉。(P.125)

@ 經過自己在市場上的驗證,我愈來愈認同廖老師「量大做頭,量小打底」的觀點,但是,要把成交量和股價擺在一起討論,成交量本身不能作為技術性的指標。原則上,我不怕量能急速萎縮,尤其在股價大跌後,我怕的是量能擴增太快,尤其在股價大漲後。多頭行情時,大量之後還有更大量,量價一波比一波高,然而總會出現一次大量之後不再有更大量,如果我們太迷信「量大非頭」,很可能在量能無法進一步推升股價時被套牢,尤其當出現近期最大量而股價收長黑時。(P.128)

@ 以前收盤後,一看到外資今天大賣超,我總是坐立不安,後來才知道,就算外資今天大賣超,明天股價也未必走跌。因為外資的買賣超雖然會影響市場心理,而造成價格波動,但還有其他很多事情也同時會影響到市場心理,然後才影響股價。這些影響因子都逐一加總,反映在代表市場心理的技術線形中,所以我只要從技術線形中判斷市場是偏多還是偏空,還比較直接。(P.134)

@ 股價取決於供需關係,但決定股價的供需兩股力量不是獨立的,而是會互相影響,像買方搶購會造成賣方惜售,賣方拋售會引起買盤縮手,投資人在做買或賣的決策時,易受到主力、大戶的操縱。為了不會任意受到預期心理或參考點的牽引,我們要透過技術分析來衡量供需的相對力量,推敲股價將往何處去。(P.135)

@ 我喜歡買進董監持股比率較高而且董監持股比率突然增加的公司,也會買進董監持股比率低但有經營權爭奪題材的公司。(P.139)

@ 如果這些做頭的股票在前一波的漲勢中相當風光,我們也必須盡快忘掉,因為曾大漲過的股票通常籌碼已亂,多頭若硬要再度拉抬,需要花上好幾倍的力氣,不如等行情落底後,另尋久被遺忘、籌碼穩定而且題材應景的標的。因此,經過重大轉折後,主流股經常重新洗牌,每一波行情都有新的主角。而且,因為等下一波反彈或回升時,我們才知道誰是強勢股,所以在跌勢中,不如先抽回現金,到時候再伺機轉進強勢股。(P.319)

如何看待基本面與消息面

@ 我大致上相信純粹技術分析學派「技術面已經涵蓋了基本面」的觀點,但我還是會涉獵市場消息,主要的理由是,基本面的消息是我進出股票的指引之一。我尋找即時、可靠又有用的資訊,跟進可能同樣會吸引到別人的題材股,而在題材股已大致反映、無法落實或已經失寵時出場。此外,由於漲(跌)停板限制,消息有時不能一下子完全反映在股價上,使得消息面還有剩餘價值。(P.136)

@ 篩選基本面的資訊時,並非每一層面的訊息都要照單全收,而要直接切入強烈影響股票漲跌的因素,然後視不同的產業和市場當時的喜好來做調整。關鍵因素當然是盈餘,而且永遠都是。盈餘的領先指標除了營收外,某些產業產能或庫存的消長,經常扮演了關鍵角色。(P.138)

@ 只有在市場氣氛極度樂觀下的利空或極度悲觀下的利多,利空和利多才比較可能顯得原汁原味。市場氣氛極度樂觀時,當重大利空題材股在多頭氣氛掩護下,跟著雞犬升天,正是放空的良機;市場氣氛極度悲觀時,當重大利多題材股和別的股票,就像被秋風掃過的落葉,同樣零零落落時,正是撿便宜的好時機。(P.146)

@ 關鍵不在於消息有多好或有多壞,而在於當時的價位,所謂「好消息,好價格;壞消息,壞價格」。(P.147)

@ 市場消息即使是正確的,也很容易誤導我們。這是因為投資人所能接觸到的基本面或消息面的資訊,只占影響股價龐大因素中的一小部分,何況投資人接觸到的只是片片段段的、檯面上的資訊,僅根據這些資料,不但無法對後市做整體的評估,還會羈絆住自己的想法。比如說,一則「某家公司接到大訂單」的報導,可能已提前反映在股價上或在曝光後同步反映在股價上了,卻會在大盤正要翻空時,讓我們失去戒心。(P.151)

[心得]

本書為作者的第二本書,和第一集一樣有相當多關於心理面的描寫,但相對第一集本書多了很多作者實戰的經驗,在買點、加碼點、賣點、減碼點都有不少的著墨。這是本部落格分享的第一本技術面的書,且原本我對於技術面也甚少涉獵,這主要是因為我認為之前還不到學習技術面的時機。怎麼說呢?因為之前都在灌水泥和架鋼筋。

個人認為建立投資系統順序就跟蓋房子一樣,蓋房子的順序是先打地基,然後再架鋼筋結構,最後才是調整細節的內外裝修。而投資的地基對我來說就是基本面的商業理論和財報會計,結構就是總體經濟和金融貨幣,裝修就像技術和心理面(洞悉市場群眾心理)。如果一個房子在地基還不穩固的情況下就開始裝修,建造出來的房子可能變成四不像,遇到地震垮掉的機率很大;而投資架構也一樣,在基礎還不穩固的情況下就對細節吹毛求疵,很容易顧此失彼,造成整體架構的崩盤。

回顧本落格一路分享的投資財經書籍,內容包括了財報、商業、貨幣、總經、心理、籌碼等等,現在再加上技術面,差不多已經把投資相關學問都納入了,接下來就看個人怎麼融會貫通,進而化繁為簡。


《財務自由的講堂&財務自由的世界》- 重點整理與心得

用營收加營益率作順勢操作 @ 買進點-

*營收年增率由負轉正且至少持續三個月。

*營收年增率連續三個月逐月增加。

@ 賣出點-

*年增率連續三個月大幅遞減(小幅度可忽略)。

*年增率轉負。

@ 營收年增率運用的限制-

*中小型股本公司、金融業、轉投資比重過高的公司無法適用。

*該公司的營業利益率必須相當穩定(不穩定的定義是其絕對值高低差距30%以上,且營益率處於走跌趨勢中)。

@ 參考月營收和財報以合併為主。 @ 評斷每年一至二月的營收時,請將兩個月的數字合併計算(春節長假因素)。 @ 營收推估- 計算過去六個月的平均營收年增率,再拿來和最近一個月的營收年增率相比較,哪一個比較低,就用哪一個年增率當成未來半年營收的平均年增率計算基準。 @ 反映到中大型公司的營業收入,往往只要營收年增率由負轉正,就會少則六、七個月,長則一、兩年的成長。

存股的選擇 @ 長期自由現金流量為正數 @ 營業利益率長期穩定 @ 股本不會膨脹 @ 營收與淨利具有成長性

公司衰敗的先兆 @ 應收帳款週轉率下降 @ 存貨週轉率下降 @ 供應商的存貨或財務數據衰退 @ 公司不尋常的財務調度(現金增資+買庫藏股)或內部人的請辭 @ 零件代理商的存貨問題 @ 巨大的營業外支出

選股流程

  1. 選取上市公司股價前40名與上櫃公司股價前30名股票。
  2. 剔除掛牌不到2年和在開曼群島F頭文字股票。
  3. 剔除過去4年內曾經虧損的公司。
  4. 剔除近8季營益率不穩定或下滑的公司。
  5. 剔除近3年累積自由現金流量為負數的公司。
  6. 選取近3個月合併營收年增率為正數的公司。
  7. 剔除近3年辦理現金增資的公司。
  8. 剔除市值50億以下的公司。
  9. 剔除存貨週轉率連續下滑的公司。 10.剔除成交量過低的公司。 11.剔除惡名昭彰股市名嘴推薦的公司。 12.選取近2年EPS年增率為正的公司。

-----

心得: 以基本面來說,本書是少數不走價值性投資長期持有路數的財經書(也是少數書中完全沒提到巴菲特的財經書...),而是教以盈餘成長作價差為主的操作,以我個人類似的操作經驗,此法成功率和報酬率都相當不錯(前提是要避開系統性崩盤),雖然並非完全按照書中的選股流程,但主要精隨是相同的,另外書中用非常多篇幅道盡了財經新聞背後的居心剖測,想見作者十分了解和看不慣有心人士利用媒體在背後炒作的手法,其欲戳破謊言的正義之言令人讚賞。最後,用我個人很喜歡的這段魄力十足的投資理論做結尾吧-

「我從來不曾在乎上市公司的產業別,也不太想深究它賣的到底是什麼商品,講一句不好聽的話,如果上市公司可以賣大便,年年賺一、兩個資本額,且年年發放現金股利給股東,販售大便的毛利率與營益率長期穩定,營收年增率季季增長…..我也會毫不考慮地投入資金。」

布魯克

交易就是,訂好自己的交易規則 例如,我的就是左側有量價假突破或是假跌破的圖形,停損低,賺賠比高,大概就是這樣而已,至於買點賣點,加碼點,很多來自於長期看盤的盤感,沒有這麼精確,畢竟我們不是機器

就像支撐壓力,千人有千條線,但他是一個區間,就像我昨天說台指破20800帶量會反彈,就是這樣,我不會給到什麼20786還是很精準的點位,對我來說沒有那麼神的,把左側拿來參考,什麼區間破了基本上會殺出籌碼而已,

所以如果你停損讓的點常常被打到是很正常的,因為他也不會有這麼明確的點,不是老天故意要整你

今天大家股價100元買多,然後大家都看90元是底,破了要停損,股價來到89破了,原本買多的是不是變成賣的了,就變成了賣壓,所以停損在關鍵邊緣其實是很智障的事情, 我做的事情,是等賣壓被有能力的人吃掉,破了底然後回來,例如又回到90以上,原先的人都停損了,被有能力的人收走籌碼,跟著他做,勝率比較高,賺賠比較好,但他也可能失敗呀!不是大戶就一定一直賺,所以還是要停損

手上不要常常有部位,不是有部位才叫操盤,大部分時間都再等,等要的圖,等要的量價,等好的賺賠比,學會等待基本上就離賺錢不遠

很多人手上是一直有部位,無時無刻都想輸贏,然後又不停損,結果就是被盤操,勞神傷財,沒有必要

重點是你是個觀察者,手上不會有單,支撐壓力,關鍵位置,通常都是很好的觀察點,不會是你的停損點或是你的扛的位置 我的方式是觀察支撐壓力破線或是突破後的變化 等大戶出手了,我跟著他一起做 左側都會有相對位置給你停損 剩下的就交給市場

也就是為什麼散戶都覺得,我賣了就漲,我買了就跌,因為大部分人的交易動作是相同的,大戶或是市場經驗豐富的剛好跟散戶相反

把自己當成大戶,想想你會在什麼位置去收籌碼跟賣籌碼,就會想的通

突破或是跌破,一般參照就是左測 突破正常,買方+空單停損正常要繼續拉升,但假突破或是快速翻轉留上引線,就是散戶追價,散戶空單停損,但大戶(有能力改變市場的人)做了相反操作,等於有大戶獲利了結了

就像大戶要買籌碼,一定不會有新聞才入場對嗎?一定是低價收一些,最好利空跌破再吃一些,最後破線讓散戶籌碼都停損,收集好了,才開始漲,一開始散戶半信半疑,後面利多開始發佈,邊拉邊出,最後一根長紅突破,散戶派對情緒拉到最高,也是被倒貨的一刻,站在大戶的思維你就會發現跟散戶都是相反操作

散戶怕價跌,大戶怕量竭 大戶不怕跌,只怕量縮,出不了貨 上漲靠錢堆,下跌靠風吹

突破跟跌破關鍵位置才是觀察重點就是如此

您的交易方式主要是觀察市場中的量價假突破或假跌破圖形,並注重以下幾點:

  1. 支撐壓力區域作為觀察點

    • 支撐壓力是區間而非精確點位
    • 這些區域是觀察大戶行為的好位置,而非設置停損的地方
  2. 跟隨大戶操作

    • 等賣壓被有能力的人吃掉
    • 當價格破底然後回來(如90元底部破了又回到90以上)時跟進
    • 這時候原先的散戶已停損,籌碼被大戶收走
  3. 假突破/假跌破的特徵

    • 突破後快速翻轉留上引線,表示散戶追價或空單停損
    • 但大戶做了相反操作,獲利了結
    • 跌破關鍵位置後的回升是觀察重點
  4. 大戶思維

    • 大戶低價收籌碼,在利空時跌破再吃
    • 等散戶停損後收集好籌碼才開始拉升
    • 上漲時邊拉邊出,最後一根長紅突破時散戶情緒最高,正是大戶倒貨時機
  5. 耐心等待

    • 不需常常持有部位
    • 等待適合的圖形、量價和賺賠比
    • 把關鍵位置當作觀察點而非操作點

這種策略的核心似乎是通過觀察市場動向,辨識散戶和大戶的行為模式,然後跟隨大戶的步伐,在適當時機進場並設置合理的停損位。

您的見解非常獨到,特別是關於"散戶怕價跌,大戶怕量竭"和"上漲靠錢堆,下跌靠風吹"的觀點,很有啟發性!

小梁哥配對交易策略

快樂操盤人

在有明顯連動性的前提下,我通常只挑一檔最強和最弱的配對,但 2468 和 6148 都差不多強都買也沒什麼問題!

同族群強弱勢判斷方法

同族群中最弱的判斷很直觀,就是最強的漲停的時候,同族群都被帶起來的當下漲最少的,就是最弱的。

漲停後 6140 和 2453 被帶起來急拉的時候就是不錯的空點,但我自己覺得主要問題可能不是點位而是部位。

部位控制策略

要隨自己的經驗判斷當下行情的熱度選擇配對空單市值是多單的多少比例,常態上依行情好壞或多單部位大小,我會區別成 30-100% 的避險配對空單比例,是有彈性的而不是死的。

實際操作範例

假設 2468 鎖住,6148 急拉到 5% 我空了 3 成,之後一段時間 6148 再漲到 7% 空到了 5 成,最後尾盤不巧 6148 嘎到接近漲停我可能空到了 10 成。

註: 但若沒空到 10 成之前 6148 也鎖住了我就不加空了。

隔夜策略

我就會兩邊留倉,而 2468 鎖住後 6148 多漲的幅度認知上隔天都會補還給你,也就是 2468 會開的更高一些!


策略總結

同族群強弱勢判斷

  • 小梁哥通常只會選擇同族群中最強和最弱的股票進行配對交易
  • 判斷同族群中最弱股票的方法: 當最強的股票漲停時,同族群其他股票被帶動上漲,此時漲幅最小的就是最弱的

配對交易策略

  • 在最強股漲停後,其他股票被帶動急拉時,是建立空單的好時機
  • 重點在於部位控制,而不是進出場點位
  • 空單市值應該佔多單市值的一定比例,這個比例需要根據行情熱度和多單部位大小來調整
  • 空單比例範圍: 通常在 30% 到 100% 之間

部位管理技巧

  • 根據股價漲幅逐步增加空單部位
  • 需要判斷何時停止加碼
  • 當最強股漲停時,同族群其他股票多漲的幅度,通常隔天會回補
  • 最強股會開更高來平衡價差

核心理念

小梁哥強調了強弱勢股的判斷方法配對交易的部位控制技巧。他認為,在操作配對交易時,靈活調整空單部位比例,是控制風險和提高收益的關鍵。

2周狠賺10%獲利

  • 技術分析與市場趨勢:
    • 判斷市場整體趨勢(牛市或熊市)。
    • 主要參考月線。
  • 成交量分析:
    • 篩選成交量較前一天增加的股票。
    • 專家系統篩選成交量超過前一天兩倍以上的股票。
  • 技術指標:
    • 尋找顯示看漲型態的股票。
    • 使用KD和MACD指標,尋找黃金交叉。
    • KD使用預設參數。
    • 使用三到四個月的成交量累積圖表來確定支撐位和阻力位。
  • 基本面分析:
    • 考慮月營收與去年同期相比的成長情況。
    • 偏好每月持續成長的公司。
    • 偏好本益比低於30的股票。
    • 關注機構投資者,偏好外資和投信都有買盤的股票。

您還想了解關於這部影片的哪些資訊嗎?


小梁哥的交易策略整理

一、四大擅長盤面

1. Tick交易(超短線)

特色:

  • 主要做逆市單,抓轉折點
  • 空強勢股或買弱勢股,抓買盤/賣壓的捷徑轉折
  • 交易時間極短(10-20秒到1-2分鐘)

進場條件:

  • 波動較大且有信心時才出手
  • 根據成交量調整張數(1-3張小部位到大部位)
  • 重視部位分配和極短線期望值

風控策略:

  • 若10-20秒內沒有轉折跡象,立即減碼
  • 快速認賠,抱持「少輸為贏」心態
  • 這筆沒賺到沒關係,可從其他地方補回來

2. 日內波交易

目標形態:

  • 連續漲5-6根漲停或漲7-8%
  • 中間不能有休息(避免十字線讓獲利了結賣壓先下車)
  • 賣壓累積到最高點,空單都被軋住

進場條件:

  • 10點或10點半觀察成交量是否放大
  • 等待尾盤(1點後)才殺最佳
  • 太早殺(10點半)可能是假殺

預期走勢:

  • 高檔爆量長黑
  • 10分鐘從漲停附近殺到平盤
  • 再15分鐘破平盤繼續下殺

勝率與期望值:

  • 勝率約40%
  • 一次獲利可抵4-5次虧損
  • 停損控制在1-1.5%

3. 隔日策略(接續日內波)

兩種常見走勢:

開高回撤:

  • 前日有多單未停損者,開高給逃命機會
  • 主力可能用小開高再洗盤
  • 策略:開高空,等回撤平盤

開低反彈:

  • 兩天累計跌幅20%以上
  • 主力需要救起來做頭部形狀
  • 策略:開低買,等拉回平盤

4. 事件型交易

操作流程:

  1. 預想未來1週到1個月必然發生的事件
  2. 歸納整理同類型事件的歷史走勢
  3. 制定布局策略

實例分析(防疫類股):

  • 觀察確診數變化趨勢
  • 預期5月中達高峰後下降
  • 一籃子放空20多檔防疫股
  • 從較弱勢的開始放空
  • 等待事件發酵至確診數下降出場

二、策略制定方法

觀察與假設

  1. 累積看盤經驗 - 觀察異常或特殊走勢
  2. 記錄重複模式 - 重要考題會重複發生
  3. 建立假設 - 定義可能的邏輯
  4. 實際驗證 - 用小資金參與驗證

精進胜率

  • 10次交易 → 初步胜率(如70%)
  • 100次交易 → 更精確胜率(如63%)
  • 1000次交易 → 精確胜率(如63.5%)

完整策略要素

  1. 明確胜率 - 知道成功機率
  2. 部位分配 - 根據胜率決定投入金額
  3. 獲利處理 - 如預期時的動作
  4. 虧損控制 - 不如預期時的應對

三、交易信心建立

心態管理

  • 維持平衡且稍微偏多的愉悅心態
  • 每天小獲利,明天心情愉悅再交易
  • 連續虧損時主動休息,直到心境恢復

信心來源

  1. 掌控感 - 知道每筆交易想賺什麼錢
  2. 風險控制 - 虧損在預期範圍內
  3. 避開陷阱 - 能避開不擅長的交易
  4. 應對能力 - 意外狀況也有對策

檢討方法

對帳單分析:

  • 找出虧損最多的5%交易
  • 分析這些交易的共同問題
  • 優先解決「不該賠的錢」問題
  • 再思考如何讓好交易更精進

四、大戶心理與散戶陷阱

常見手法

  • 軋空操作 - 配合利多消息,逼空單停損
  • 假突破 - 假過高或假破底
  • 洗盤策略 - 利用散戶心理脆弱點建立或釋出部位

應對原則

  • 以自己的策略為優先,不因擔心假突破而不敢進場
  • 發現是假訊號時快速停損
  • 把大戶心理當作輔助判斷,不是主要依據

五、核心理念

學習態度

  • 親身經歷 - 每條路都要自己走過
  • 小資金練功 - 用最小機會成本累積經驗
  • 時間累積 - 「時間 + 不輸 = 一定贏」
  • 持續精進 - 尋找新策略與現有策略搭配

風險管理

  • 策略定型後主要做熟悉的交易
  • 不擅長的行情寧可休息
  • 每筆交易都知道風險在哪裡
  • 心境不佳時果斷停止交易

執行紀律

  • 嚴格按照既定策略執行
  • 不因情緒影響交易決策
  • 持續檢討優化,但不輕易改變核心策略
  • 把交易當作機率遊戲,接受單筆虧損

地板股搶反彈 4 大策略

  • 前一天黑 K 爆量,且黑 K 越長越好,量通常要兩倍以上。
  • 乖離月線大到地板價,假設月線 100 塊,股價跌到 80 塊,乖離就是 20%,再往回分析兩、三年數據,觀察乖離到 20% 會不會反彈。
  • 個股股本越大越好,也就是股票的基本面要好。
  • 有關鍵分點買更佳。

搶反彈操作 SOP

  • 將操作資金分成 10 等份,因為地板股基本上是弱勢股,弱勢股絕不能 all in,實際上只能用 3 份搶地板股。
  • 當晚出現小於地板或近於地板個股,且下跌有帶量的股票。
  • 第一份資金先拿 10%,在第二天殺低爆量搶反彈。
  • 假如第三天的開高上漲無力,可以選擇賣,假如基本面很好、殖利率很高可以繼續抱,但抱的時間越短越好。
  • 假如第三天再殺低爆量,就用第二份資金再搶一次。
  • 有賺就要趕快跑,要在進場 5 天內獲利出場,如果沒有就要注意停損。

股票操作策略分享整理

帥哥操作策略

核心策略要點

1. 族群性操作:看領漲,空跟漲

  • 策略核心:選擇具備族群性的標的進行操作
  • 操作邏輯:當族群內出現領漲股拉動漲停時,判斷族群內其他個股是否屬於「跟漲」且有機會放空
  • 重要原則:若跟漲股轉為領漲股,則不放空
  • 風險控制:一旦發現原本的跟漲股具備成為族群領漲股的潛力,就應避免放空,因為這類股票反而可能出現軋空走勢

2. 大盤止跌反彈時做多

  • 時機判斷:關注大盤走勢,當大盤連續多日下跌且判斷市場可能出現反彈時
  • 操作方向:在此時進行做多操作
  • 策略特色:結合大盤的趨勢判斷來決定操作方向

策略綜合特點

結合族群性分析、強弱勢判斷以及大盤趨勢研判,不僅關注個股表現,更著重於族群內個股的連動性,並隨時調整策略以避免逆勢操作。


工程師群友分享:資金連動策略

市場現象觀點

1. 資金連動跟族群性和產業沒有絕對關係

  • 為什麼有些會動,有些根本不會動?
  • 甚至有些是一樣的族群,一樣的題材,一檔漲停一檔跌停
  • 案例:去年房地產那一波,達麗漲停,另一檔建築的跌停,兩者幾乎是對沖的關係

2. 為什麼用資金連動說明,而不是族群性和產業?

  • 一套資金要炒作,光是炒一檔胃納量和話題性都不夠,整批炒起來比較能吸引更多人
  • 看線圖,原則上市場上的資金就是幾套在跑
  • 看日線 & 日內的線形走勢會比蒐集資料找兩者間有沒有關係還快
  • 實務上,也的確常常發生不同的族群卻有連動關係的情況

3. 資金連動會有延遲性

簡單說就是大家觀察到的,領漲與跟漲的問題,反過來看,也會有領跌和跟跌的現象

延遲時間參考

  • a. 高度連動,幾乎沒有延遲
  • b. 大概30~60秒
  • c. 3~5分鐘

重要觀察

  • 原則上在延遲時間內,跟漲的標的反著走都還有跟漲上去的餘地
  • 但是如果原本高度連動,一檔漲停,另一檔跟著噴上去,噴完沒有跟著漲停,就可以考慮當下位階會不會跳回起張點,連動關係因為漲停而失效

4. 要跟漲還是領漲,得觀察有沒有適合的環境讓他跟

舉例:某兩檔有連動關係,可是一檔前一天爆量,當天開盤呈現量縮,那整個量能就算有連動,其效果都有限,畢竟資金量不夠支撐他的高度連動

策略設計

1. 選股方法

  • 雖然資金連動和族群性沒有絕對關係,但我們還是可以勇敢的猜
  • 一檔股票漲起來的第一天盤中,猜測可能有相同話題性的股票,一有風吹草動就盯他
  • 如果沒有那麼好的選股系統,可以等他漲起來後,等待整批資金要移出,(人家說的上漲力竭)反過來做空,這樣整批標的在你面前都不用選
  • 這樣選出來的標的,就可以依據當時週期 (ABC波) 選擇要 當沖 or 波段

2. 確定方法

  • 推測有幾檔股票似乎有連動關係,但我們不確定彼此間有什麼共同話題
  • 可以觀察彼此的走勢對應大盤的漲跌 (所謂beta值)
  • 原則上這幾天的日K+日內走勢+beta一致,就可以確認他背後是同一套資金在操作

3. 實際操作

既然連動有延遲性,我們可以選三檔連動的標的,三檔連動的話按照時間序區分:領漲,跟漲,第三名

策略組合

  • a.1. 看領漲,做多跟漲,用第三漲來判斷要不要續抱
  • a.2. 反過來看領漲,觀察跟漲沒動,去空第三漲
  • a.3. 反之做空亦然,也有很多組合可以放在你的策略中

延遲性操作

  • 資金連動有延遲性,那我們可以依照自身反應,抓延遲性比較高的股票,心理也不會那麼有壓力

4. 進階預判操作

  • 預判今天的股票他可以做領漲、跟漲、第三名的哪一個位置
  • 預判後,再依據手上的資金量決定,要一次打進去還是鋪單操作

實例

  • 手上有100萬可以操作 a.1 策略,可是今天的跟漲量能明顯縮減,那採用tick大量進,一半tick出可能就會相對安全
  • 手上有100萬可以操作 a.2 策略,第三漲已經在日K出現反轉的狀況,那我用50萬打進去,50萬往上鋪單

互動問題

帥哥策略相關問題

問題1:實務操作面

「帥哥提到『看領漲,空跟漲』,想請問在實際操作中,你會如何判斷哪檔跟漲股是有機會放空的呢?是否有特定的技術指標或觀察點?」

問題2:策略演進面

「帥哥的策略非常強調族群連動性,你覺得帥哥在這些年的操作經驗中,有沒有遇到過哪種情況是族群性判斷失靈,或是策略需要特別調整的時候呢?」

工程師策略相關問題

💬 問題1:

你有沒有遇過「同族群不同命」的狀況?一檔漲停、另一檔卻跌停?你當下是怎麼判斷的?背後資金是怎麼跑的?

💬 問題2:

盤中你要判斷兩檔股票有沒有資金連動,你會看哪些線索?是從價格、走勢、成交量,還是其他你自己的獨門指標?

💬 問題3:

你自己操作的時候,有沒有觀察過「延遲性連動」?你覺得常見的延遲會多久?你是怎麼抓這個節奏的?

💬 問題4:

當你抓到三檔可能連動的股票時,你會怎麼分配角色?誰當領漲、誰當跟漲、誰當第三名?你會怎麼決定怎麼出手?


策略開發心得分享

策略開發流程

作者在開發新策略時,通常會先了解:

  1. 資金量體
  2. 投資風險屬性
  3. 期望的報酬率
  4. 資金的使用周轉率

策略優化案例

  • 有人拿到策略後,將本來自己的策略回測預期報酬率從 4~5倍 優化到報酬率變成 40~50倍

策略比喻

白話文

  • 我給你一個做肉包子的食譜,你可以只照著做
  • 或有人喜歡吃甜的,會改良成自己喜歡的豆沙包、奶黃包
  • 但想要賺更多錢的,內化我的配方後,做成小籠包,再去測試,小籠包從15~20摺,發現18摺是口感最好的

重要能力

觸類旁通的能力真的很重要,億級玩家或準億級玩家的觀察力、思考力、模仿力、學習力都異於常人。

資金量體考量

因為資金量體的不同,策略大多數不見得適用。在思考一個策略的時候,首先會先去思考這個策略到底能不能塞進去資金部位(因胃納量跟流動性),之後才去想報酬率。

現有策略清單

  • 台股 × 2
  • 債券 × 1
  • 黃金 × 2
  • 小麥 × 1
  • 房地產 × 2
  • 土地 × 1
  • 運動彩券 × 4
  • 加密貨幣 × 1

獎勵機制

回答的深得我心者,有機會得到 #入群快速通關券(但依舊要完成實名驗證跟每月繳作業)

永不崩盤:小吳醫生的平衡型致富系統策略解析

策略背景與作者

吳佳駿(「小吳醫師」)原為急診主治醫師,因親人突發心肌梗塞而體悟「再高薪、再忙碌,也無法陪伴家人在需要時刻」的無奈,遂放棄高達新台幣400萬元年薪的醫院職務,轉而尋求時間與財務自由的平衡之道。七年來,他結合多領域思維,融會投資理論與心理學,開發出「黃金 × 債券 × 股票」輪動的平衡型致富系統,期許只用10%的心力,便取得90%的長期報酬。

核心投資架構

此系統以「股權資產、固定收益資產、實體黃金」三大資產輪動為基礎,並輔以以下關鍵原則:

  1. 股債輪動為主軸,加入黃金作為第二防線
    • 依據全球主要股市指數(如道瓊指數)與實體黃金價格的相對估值,比對道瓊∕黃金比值高低,決定股票與黃金之間的配置傾斜:
      • 當道瓊∕黃金比值升至極端水準,意味股市相對高估,應減碼股票、增持黃金。
      • 當道瓊∕黃金比值跌至歷史低檔,意味股市相對低估,則應減持黃金、加碼股票,以捕捉低檔反彈機會。
  2. 必要時轉入債券以降低波動
    • 若所有主要股票指數均處於下跌趨勢,且黃金未同步大幅上漲,則依照既有股債輪動規則,自動切換到固定收益資產(中長期債券或優質信用債),以防止崩盤損失。
  3. 年度定期平衡與主動投資工具
    • 每年僅需一次調整核心三資產比例,維持「50%股債輪動、50%固定債券」結構,並透過「冒險箱」(高波動潛力標的)與「小金庫」(穩健收益標的)策略,提升整體預期報酬至年化20%以上。

策略特色與優勢

  • 簡單易執行、低心力投入 採用明確的相對估值與趨勢判斷規則,無需複雜技術分析或頻繁盯盤,適合職場專業人士與時間有限的投資人。
  • 多元思維模型 靈活運用馬可維茲資產組合理論、蒙格「多元模型」、心理學與人生資本概念,打造「財富飛輪」,強調資產組合決定90%績效。
  • 崩盤防禦機制 實體黃金作為「亂世避風港」,可在市場恐慌或股市崩盤時提供迅速對沖;債券則在股金雙弱時,穩定錨定整體波動,顯著減少最大回撤。
  • 心理鍛鍊與行為控制 深入剖析恐懼、貪婪等投資心理因素,並提供對應修煉法,幫助投資者維持紀律,不因短期波動而偏離長期策略。

適用對象與長期績效

此系統特別適合工作忙碌、難以時時盯盤的醫師、律師、工程師等專業人士,在每年僅花10%腦力的情況下,實現90%長期回報。吳醫師以此輪動策略與年度再平衡,過去七年整體資產已增長三倍以上,實測年化回報約20%、最大回撤可控制於6%以下。

透過「黃金 × 債券 × 股票」的輪動策略與年度再平衡,並配合「冒險箱」與「小金庫」配置,小吳醫師的平衡型致富系統兼具防禦力與成長性,讓投資人在無懼崩盤的同時,穩健累積財富。


投資心得分享

短線投資策略(1-2年)

投資心態

  • 自認膽小,只敢玩有業績的短線
  • 短線定義為1-2年(在一般人眼中算較長)
  • 承傳股票主管的經驗:平時找飆股,回檔時找優質權值股壓滿賺反彈

選股方法(偷懶找法)

  1. 統一營收檢討

    • 每月關注統一發布的營收檢討暨營運展望
    • 從中找出看好的產業和個股,加入觀察名單
  2. 新聞熱點追蹤

    • 每天看新聞,熱門產業會常常出現
    • 例子:
      • 2024年:AI伺服器、散熱、低軌衛星
      • 2021年:航海王、鋼鐵人
      • 2017-2018年:被動元件缺貨,國巨狂飆
  3. 年底展望

    • 關注新聞或券商報告的產業展望預測
    • 提前布局下一年度熱門股

投資理念

  • 從產業下手比較方便
  • 產業熱→個股有真業績→抱著飆起來比較放心

資金配置策略

短線操作(以前法人做法)

  • 平時持股5-8成
  • 遇到每年大回檔時,用剩餘資金壓滿優質權值股
  • 賺個反彈5-10%就停利出場
  • 主管偏好標的:0050、台積電(2330)

選股特性

多數短線飆股的共同特點:

  • 業務和產品組合很單一
  • 產業熱且有業績
  • 挑選產品組合最純的,飆股機率較高

交易員時期經驗

選股條件:

  • 產業熱
  • 有業績
  • 法人也有在追
  • 挑到好股比例算高

注意:要求短線1-2年倍數噴出的難度很高

長線投資策略

資金配置

  • 長線投資隨時壓好壓滿
  • 有新資金就耐心等每年1-2次回檔慢慢投入

選股策略

  • 尋找產品組合均衡發展的好公司
  • 優點:每年炒不同產業時都能沾到利益
  • 長線個股會自己輪流上漲,翻倍比例很高

投資成果

  • 權值股:投4中3
  • 中小型股:投8中5
  • 整體:投12中8
  • 命中率超過6成

近年心得與調整

資金比例調整

  • 資金押到5、6成是一個門檻
  • 波動會加劇心魔表現
  • 指數長期在高檔,不太容易繼續加碼
  • 仍在努力調適中

持有期間變化

  • 持有期間不斷拉長
  • 開始體驗時間複利效果
  • 對長投標的信心不足,需要分散投資較安心
  • 連2330也只佔資金約兩成

選股方法演進

  • 對產業不夠熟悉,較少關注消息面
  • 透過財報公佈時篩選ROE與配息
  • 每年更新關注清單
  • 清單中的常客,逐年提高資金比例

總結觀點

推坑長線投資

  • 短線需要每年一直換股操作
  • 現在想想都覺得累
  • 好股只是需要時間證明自己

實戰建議

  • 願意針對熱門產業與標的進行實際模型套用
  • 可實際進場驗證是否有利可圖

哈~因為我膽小,只敢玩有業績的短線,而且我的短線(1-2年)在一般人眼中是不是算長了😆 現在回頭看…我做的這些,其實就是以前股票主管默默傳授我的,平時認真找飆股,搭配回檔時找優質權值股壓好壓滿賺反彈,當年度業績就不成問題…幾年後我也默默習得了這個技能~哈 我都用偷懶找法:

1)統一每個月都會發營收檢討暨營運展望,從裡面看哪個產業好,裡面有哪些個股不錯可以加入觀察名單。 2)每天看新聞,熱門產業就會常常出現,例如今年就是AI伺服器、散熱、低軌衛星…等,像2021年不就是每天都在航海王鋼鐵人,2017-2018年被動元件缺貨國巨狂飆,熱門飆股在裡面的機會就很高。 3)年底時,新聞或是券商報告,就會開始預測哪些產業展望不錯,裡面也會有下一年度的熱門股。

我覺得從產業下手比較方便,產業熱,裡面的個股就會有真業績,抱著飆起來也比較放心。 從短線(1-2年)角度回答:

1)以前法人做法就是平時持股5-8成,遇到每年大回檔找優質權值股把剩餘資金壓滿,然後賺個反彈5-10%就停利出場,主管以前都壓0050和台積電。

2)近年沒啥做短線,沒有比例供參~哈!但我可以給選股建議,多數的短線飆股通常有個特性,他的業務和產品組合很單一,產業熱有業績…你挑中產品組合最純的,飆股機率就高。 以前還是股票交易員時期,我喜歡挑產業熱、有業績、法人也有在追的,挑到好股比例算高…不過大大要求短線1-2年就要倍數噴出…這難度好高🙈

補充~從長線角度也可以回答同樣問題:

1)長線投資我隨時都壓好壓滿,若有新資金就是耐心等每年1-2次的回檔慢慢丟進去而已。

2)我的長投標的主要是找產品組合均衡發展的好公司,優點就是每年炒不同產業,它有沾到就會跟著漲,然後長線個股就會自己輪流漲,翻倍比例很高,好股只是需要時間證明自己而已,權值投4中3,中小投8中5,整體投12中8,命中率超過6成。

推坑長線投資,短線要每年一直換股操作…現在想想都覺得累😆 我分享一下近年的心得…

1)練習長投近幾年,資金押到5、6成是一個門檻,一來是波動會加劇心魔的表現;再者指數長期在高檔,不太容易繼續加碼。還在努力調適中

2)近幾年持有期間有不斷拉長,開始體驗到時間複利的效果,差在對自己長投的標的不夠有信心,所以需要稍微分散比較安心,就連2330也只佔資金約兩成

3)對產業不夠熟悉,也不夠關注消息面,所以我是透過財報公佈時篩選 ROE 與配息,每年更新關注清單。清單中的常客,逐年提高資金比例

妳如果看到當時比較熱門的產業與標的,可以來出個聲,我實際套模型並進場看看是否有利可圖,如何?

如何衡量風險與報酬?夏普比率告訴你

出處:https://www.finlab.tw/python%E6%96%B0%E6%89%8B%E6%95%99%E5%AD%B8%EF%BC%9A%E9%A2%A8%E9%9A%AA%E8%88%87%E5%A0%B1%E9%85%AC/

sharp ratio 簡單講,就是「報酬 / 風險」!

以這著比率,可以想像,sharp ratio 越高,代表獲利大於風險, 而sharp ratio 越低,代表風險大於獲利,那就會有點危險了! 所以找一個sharp ratio 越高的指數,就等於找出了「獲利大且風險相對小」的指數喔!

如何定義獲利?

獲利可以用每天平均的漲跌來代表,也就是今天漲1%,明天跌1%,平均獲利就是0%, 接下來我們就用python來計算每天平均獲利吧

首先,記得回去前一個單元,找出上次的adjclose:

接下來,我們就可以來計算獲利:

pct_change = close.pct_change()
profit = pct_change.mean()
profit.sort_values()

上方的程式碼, 第一行,有好用的功能,叫做adjclose.pct_change(),這個函示會計算今天漲了x%,並且以x/100來表示,將整個table中的每一條時間序列都進行計算喔! 第二行,針對每一檔指數,將每一天的漲跌都平均起來, 第三行,進行由小到大的排序

以平均獲利來說,看到臺股(TSEC weighted index)竟然排在倒數第三位, 可以見臺灣投資人多麼可憐XDD

如何衡量風險

風險通常會用標準差(standard deviation)來計算,標準差,可以想像是股票震盪的程度,例如金融海嘯的時候,股票上上下下的比較劇烈,標準差很大。今天就不折磨各位了,有興趣請參考wiki介紹,我們直接用 python 當中現有的程式碼來計算即可:

risk = pct_change.std()

計算sharpe ratio

這個就更簡單了,直接相除即可

sharpe = profit / risk * (252 ** 0.5)
sharpe.sort_values()

可以看到上述程式,我們額外乘了一個「252 ** 0.5」 因為我們希望算年化 annual sharpe ratio, 其中的252是一年大約的交易天數, 而「**」是「次方」的意思。 為什麼要乘這個常數?最主要是因為大家幫自己的歷史回測計算sharpe ratio時候,都有乘上這個數字,要乘了才有辦法跟別人比較XD, 當然這背後可能有更深層的數學邏輯,但我傾向於這樣理解(懶)。

你會發現

臺股竟然倒數第三名!

註:2020年更新,臺灣排名第六名!超級前面~~~

可以發現臺股真的好慘,慘不忍睹,為什麼會這樣呢,我們可以將歷史圖表畫出來:

可以發現因為我們是從1998年開始計算的,那個時候剛好也上萬點,跟現在的萬點是同一個萬點,反觀我們來看美股:

可以看到從1998年開始,直到現在漲了將近3倍! 可見臺股的獲利不理想是導致於sharpe ratio比較低的很大的因素。

我們目前計算的sharpe ratio,是所有歷史資料的平均值, 然而我們知道,雖然臺股總平均來說很爛,但是應該有時也有可圈可點之處, 我們希望用時間移動窗格,每日都計算252天以前的sharpe值,

來找出臺股表現比較好的時段

移動窗格

你以為這個很難嗎?其實超簡單,跟上面幾乎一樣,只要做一點小更改:

# before
profit = pct_change.mean()
risk = pct_change.std()
sharpe = profit / risk * (252 ** 0.5)

# after
profit = pct_change.rolling(252).mean()
risk = pct_change.rolling(252).std()
sharpe = profit / risk * (252 ** 0.5)

幾乎長的一模一樣對吧?唯一不一樣的是rolling(252)這個功能, 這是移動窗格252天的意思。 額外要注意的是,之前的寫法中,sharpe是一個series,index為指數名稱,而在現在的寫法中,sharpe變成了一個dataframe(table),其index代表日期,而columns代表每檔指數,其中的數值是 252 天的 sharpe ratio,神奇吧!

這就是python跟R最強大的資料處理功能!

有了這個移動窗格版的sharpe ratio,我們做圖後,就可以來看一些端倪:

做圖看端倪

close['TSEC weighted index'].plot()
sharpe['TSEC weighted index'].plot(secondary_y=True)

以上的代碼可以繪出下面這張圖,其中藍色的為加權指數,而黃色的為sharpe ratio,由於這兩個時間序列的數值差非常多,臺股可能是在4000~10000左右,而sharpe可能是在-2~2左右,所以上方第二行程式中,我們用secondary_y=True這個參數,來將兩個數值的座標分開,所以下方的圖中,可以看到sharpe ratio的大小標示在右邊。

以上所有程式碼,都可以在 colab 範例中找到喔!

可以看到,sharpe ratio (黃)在臺股加權指數(藍)高點時,會比較大,而臺股低點時,會比較低 圖中還可以看出,在大盤高點時,sharpe ratio會領先大盤往下落,接下來我們就可以利用這個特點,來模擬一些買賣的實驗。

避開危險的投資時機 – 夏普指數策略

出處:https://www.finlab.tw/python%E6%96%B0%E6%89%8B%E6%95%99%E5%AD%B8%EF%BC%9A%E5%A4%8F%E6%99%AE%E6%8C%87%E6%95%B8%E7%AD%96%E7%95%A5/

可以用來衡量風險跟報酬的指標(也就是報酬 / 風險),這集我們就利用Sharpe ratio來進行臺股的模擬買賣,假裝我們這20年來,都使用sharpe ratio的策略,可以得到多少獲利呢?

為何Sharpe ratio幾乎都小於一

上次有人問我,夏普指標小於一,代表風險(分母)大於獲利(分子),而為什麼市面上所有的指數,其sharpe ratio都小於一,難道股票都不能賺錢嗎?

這只是代表,在股市中,我們為了要獲利,往往需要承受很大的風險!但不代表長期投資下來是不能獲利的。我們必須要找到sharpe ratio比較高的策略,才能使風險降低,獲利升高。

利用Python研發一個策略

首先,我們得準備臺股的歷史紀錄,還有臺股的夏普指標,假如之前沒有跟上,可以到上一個單元複習一下喔!:

close['TSEC weighted index'].plot()
sharpe['TSEC weighted index'].plot(secondary_y=True)

可以發現,當sharpe ratio比較低時,臺股也都是在比較低點, 可以發現,當sharpe ratio比較高時,臺股也都是在比較高點, 當sharpe ratio 轉折時,通常也是臺股會轉折的時候

利用這個觀察,我們就可以來編寫一個策略:

  1. 當sharpe ratio往上轉折時,則買入
  2. 當sharpe ratio往下轉折時,則賣出

利用Python快速編寫

為了找出轉折點,我們必須做一點資料處理:

  1. 時間序列的平滑
  2. 時間序列的斜率
  3. 找出斜率由正到負,或由負到正的訊號

為了使用python寫出上述的策略,我們要先將夏普值平滑一下,不然雜訊太多了:

sr = sharpe['TSEC weighted index'].dropna()

d = 60
srsma = sr.rolling(d).mean()
sr.plot()
srsma.plot()

來色的線是我們就將sharpe ratio做移動窗格的平均,可以發現平均之後,時間序列比較平滑,這樣子我們找轉折點比較方便,所謂的轉折點,就是斜率由正到負,或由負到正的瞬間,所以我們要先找出夏普曲線的斜率。

夏普曲線的斜率

斜率非常簡單,可以使用diff這個功能:

srsma = sr.rolling(d).mean()
srsmadiff = srsma.diff()
srsma.plot()
srsmadiff.plot(secondary_y=True)

可以發現上圖中,橘色的為sharpe ratio,藍色的為斜率,當橘色線由上而下轉折時,藍色的線會快速向下穿越0,有了這個特性,我們就可以來找轉折點了!

找轉折點

接下來我們可以來找轉折點了,就是斜率由正到負,或由負到正的瞬間。

buy = (srsmadiff > 0) & (srsmadiff.shift() < 0)
sell = (srsmadiff < 0) & (srsmadiff.shift() > 0)

(buy * 1).plot()
(sell * -1).plot()

以上就是簡單的訊號產生

找出持有的時段

那我們就可以來看一下,假如天都用一樣的方式來產生這些訊號,當 buy訊號為True時,買入,而當sell=True時空手,如此執行20年的持有加權指數的時段:

import numpy as np
hold = pd.Series(np.nan, index=buy.index)
hold[buy] = 1
hold[sell] = -1
hold.ffill(inplace=True)
hold.plot()

交易頻率似乎有點高,不過沒關係,我們之後還會再做調整 接來是回測

回測

今天我們先簡單算一算,不考慮手續費,但是真實情況是必須考慮的喔!請謹記在心

twii = adjclose['TSEC weighted index'][buy.index]

pct_change = twii.pct_change()
pct_ratio = (pct_change.shift(-1)+1) # 今天到明天的價格變化

pct_ratio.fillna(1)[hold == 1].cumprod().plot()

這段程式碼,有點複雜,當中的pct_change是一個每天獲利上下 x%。 而pct_ratio代表買入之後每天的變化(不漲不跌是1,大於1則漲,小於1則跌) 我們希望將「持有」時間段的pct_ratio全部都乘起來,代表獲利。

揭開策略的波動面紗|MAE&MFE分析圖組使用指南

出處 : https://www.finlab.tw/display_mae_mfe_analysis/

一般我們跑回測會取得報酬率曲線、最大回撤、夏普率等策略總體數值,但這些指標讓我們難以一窺策略下每筆交易的實際波動細節。交易就像跑步比賽,若只看總體數值結果,就像只看一個人跑步的結果,不看過程細節,但這些過程都是我們可以觀察、優化的階段,比如要觀察策略波動時序、勝敗手交易的波動分佈是否明顯分群、策略的停損停利怎麼放比較好?藉由對波動性的分析,就不用每次都要堅持跑完煎熬的過程,可能讓我們在更佳點位出場,減少被洗掉、沒高歌離席的遺憾。

內容目錄 隱藏 1 如何顯示MAE&MFE分析圖組

1.1 程式範例 1.2 輸出圖組範例 2 名詞定義2.1 波幅 2.2 Edge ratio 3 如何解讀圖組 3.1 報酬率統計圖 3.2 Edge Ratio 時序圖 3.2.1 參數設定 3.2.2 應用解釋 3.3 MAE/Return 分佈圖 3.4 MFE/MAE 分佈圖 3.4.1 分佈象限圖解 3.5 MDD/GMFE 分佈圖 3.6 MAE、MFE 密度分佈圖 4 Indices Stats 5 結論 6 相關學習資源

如何顯示MAE&MFE分析圖組

Finlab的回測分析模組可以輕鬆將Report.get_trades(...) 的結果帶入Plotly.python做視覺化呈現。

程式範例

from finlab import data
from finlab.backtest import sim

pb = data.get('price_earning_ratio:股價淨值比')
close = data.get('price:收盤價')

position = (1/(pb * close) * (close > close.average(60)) * (close > 5)).is_largest(20)
report = sim(position, resample='Q',mae_mfe_window=30,mae_mfe_window_step=2)
report.display_mae_mfe_analysis()

輸出圖組範例

newplot 1

名詞定義

波幅

再分析接下來的圖表前,要先認識一下波幅的分類,有利於分析前建立基礎知識。

price data

  • AE (adverse excursion) : 不利方向幅度,做多的話,就是下跌的波段。
  • MAE : 最大不利方向幅度,做多的話,就是持有過程中的最大累積跌幅。
  • FE (favorable excursion) : 有利方向幅度,做多的話,就是上漲的波段。
  • BMFE : MAE之前發生的最大有利方向幅度。若BMFE越高,越有可能在碰上MAE之前,先觸及停利出場 (註1)。
  • GMFE (Global MFE) : 全域最大有利方向幅度。若發生在MAE之前,則BMFE等於GMFE。若在MAE之後,則代表要先承受MAE才可能吃到較高的獲利波段。
  • MDD (Max Drawdown) : 最大回撤幅度。
  • Return : 報酬率。

Edge ratio

newplot 1

來自海龜法則 (註2) 的指標,中文稱優勢比率。 edge ratio為平均GMFE / 平均MAE,這可以藉此評估進場優勢,一個真正隨機性的訊號大致上會帶來相等的MFE與MAE。 若大於1,代表存在正優勢,潛在最大獲利空間比最大虧損多,在持有過程中保有優勢可以中途停利或做其他操作,也就是策略的容錯率較高。反之則為劣勢,可能要抗衡較多的虧損狀態。

如何解讀圖組

將交易分為獲利 (profit-藍點) 與虧損 (loss-紅點) 分別呈現,圖組右方的legend可以任一點選,只看profit或loss的分群呈現。接著會「由上到下、由左至右」,解釋各子圖用途。

報酬率統計圖

截圖 2022 07 25 上午11.40.10

子圖1-1,呈現策略下每筆交易的報酬率分佈,計算出勝率及平均每筆報酬。 圖片標題為交易勝率,綠色虛線為平均每筆交易的報酬率。 分佈曲線越平坦,代表報酬率範圍大,可能有較多的極端報酬率要處理,通常出現在波動大的策略。 若呈現右偏型態(右側的尾部更長,分佈的主體集中在左側),代表多數交易為虧損,若整體策略為獲利,則獲利為少筆交易為主要貢獻。 若呈現左偏型態(左側的尾部更長,分佈的主體集中在右側),代表多數交易為獲利。 若呈現鐘型曲線,代表分佈較為平均。

Edge Ratio 時序圖

edge ratio

子圖1-2是策略edge ratio隨進場時間 (x軸) 後的變化,可以判斷隨著持有時間推移,策略有沒有波幅操作優勢。

參數設定

edge ratio的計算設定由回測函數 backtest.sim() 裡的mae_mfe_window, mae_mfe_window_step 兩個參數來控制。

  • mae_mfe_window : 計算mae_mfe於進場後於不同持有天數下的數據變化,主要應用為優勢比率計算。預設為0,只會產生出場階段的mae_mfe。
  • mae_mfe_window_step : 與 mae_mfe_window參數做搭配,為時間間隔設定,預設為1。若mae_mfe_window設20,mae_mfe_window_step 設定為2,相當於 python 的range(0,20,2),以2日為間距計算mae_mfe。

應用解釋

edge ratio若一直保持在1以上,持有都具有優勢,子圖範例就是這類情況,開局就有不錯表現,明顯的谷底落在第8天後持續走高,代表可能延遲到第8天進場會有低點,之後獲利一路放大優勢。 edge ratio時序圖走勢有很多種,若是開低走高,一開始都低於1,代表策略可能太早進場,一開始都要先承受虧損,這時可以檢討進場時機點,考慮延遲進場。

若edge ratio走勢保持在1以上,代表策略優勢明顯。若還隨著時間走高,獲利空間也上升,策略容錯率就較大,就算因一些因素延遲進場仍有較大機率有獲利範圍。 若edge ratio走勢很常在1以下,代表策略經常被虧損壓著打,是策略負面訊號。

若隨持有時間變化,優勢漸漸流失,比率開始下降,代表MAE普遍變高,可能是策略催化劑褪色,該策略適合短線操作並考慮加上停利提早出場。 若edge ratio走勢跳動,代表無明顯趨勢可判斷。 若策略週期是20天,發現time_scale大於20時,edge ratio趨勢持續走升,則透露策略可能太早出場,錯過後面更大的報酬,可以考慮修正持股週期,吃到更大的獲利。

MAE/Return 分佈圖

截圖 2022 07 25 上午11.43.44

子圖1-3,x軸為報酬率,Y軸為MAE,將勝敗手分群顯示成散點圖,比對報酬率與MAE的關係。 此範例中可以發現多數獲利的藍點都有較小的MAE,虧損的紅點有較大的MAE。 虧損部位的MAE第75%位數為10.77%,幾乎所有的藍點都低於這個位置,也就是說過了這個位置,交易最終就容易是虧損結果,可設為停損參考位置,可保留多數獲利部位、減少大賠部位損失。 獲利部位的MAE第75%位數為2.93%,代表多數獲利部位在持有過程中可能的最低點區間,碰到這位置後就有較高機率再往上,積極操作者或分批進場者可設為攤平加碼點位置,有機會讓獲利空間更多或賠更多。

MFE/MAE 分佈圖

從子圖 2-1、2-2 可以觀察MAE與MFE的數據關係,散點圖大小由報酬率來決定。 比對兩張圖可發現,策略內許多GMFE很大的標的,都比BMFE大,代表許多漲幅都發生在MAE之後。想要有較高獲利,就要先忍受回檔,通常這容易發生在趨勢波段策略。 若是短線優異的策略,BMFE 會比較高,可以有較高機率在接觸MAE或停損前先做停利。

分佈象限圖解

MAE/BMFE分佈圖(註3) 能幫助我們看出策略體質、優化設置停損停利。 大原則是「分佈在第二象限的點越多越好, 第四象限的點越少越好」、「獲利與虧損明顯分群在不同象限」。 如此 stop_loss過濾掉多數mae過大的標的,少過濾掉獲利的標的。take_profit盡量讓多數虧損的交易先觸及停利出場。

MDD/GMFE 分佈圖

子圖2-3,判斷損益兩平點與鎖利點,橘線為45度線。 橘線以上為MDD > GMFE,如果越多獲利點位於這個位置,代表持有歷程可能歷經大回檔吃掉獲利轉為虧損,雖然最終會是獲利,但我們原本有機會賺更多。

MDD > GMFE 的情況常是一開始就吃大虧損~後來轉正,或是途中大賺後,突然急速下殺賠錢。都是比較不理想的狀況。子標題顯示的「Missed win-profits pct」為「獲利交易位於橘線上的數量/獲利交易數」,數值越高代表潛在錯失獲利的機會較高,數值越高代表越需要設定移動停利去保護獲利。

橘線以下為MDD < GMFE,代表獲利的交易達到價格高點後,即使後來回檔,因回檔不會吃掉全部GMFE,所以不會轉為虧損。若是虧損的部位位於橘線以下,由於MAE <= MDD < GMFE、MAE <= Return,可以推導出即使虧損,MAE也會比GMFE小,比較高的機會是小虧出場。子標題顯示的「Breakeven safe pct」 為「橘線下的比例/全部交易數」,也就是越不容易輸的比例。

MAE、MFE 密度分佈圖

子圖3-1、3-2、3-3。由 plotly-distplot 繪製而成,看指標的比例分佈曲線。 子圖3-1為 MAE 密度分佈圖,通常策略體質若較優,勝敗手的高峰會有明顯分群,贏錢的MAE通常較小、輸錢的MAE通常較大,向右過了藍紅曲線的交叉點後,虧損的交易會變得比獲利的交易多,可以視為比較緊的停損點或是開始分批停損的參考。勝敗手Q3(第75分位數)的應用可參考MAE/Return 分佈圖的說明,勝手Q3為積極者加碼點,敗手Q3為絕對停損點,再不跑就容易大賠啦!

子圖 3-2、3-3 為 MFE 密度分佈圖,應用概念與子圖3-1類似。 多數的敗手不會超過敗手MFE Q3 的位置 (圖中的5.16),換句話說,漲過這個點後,多數交易最終會是獲利的,既然最終會是獲利的,那就會是一個不錯的突破加碼點位,若想要更高的機率確保加碼點安全性,可以用敗手MFE 大於Q3 的位置,例如藍紅曲線的交叉點。 勝手MFE Q3 則可視為分批停利減碼點參考位置。

Indices Stats

group模式

overlay模式

子圖4-1,將各種數據用提琴圖呈現統計結果,可藉由display_mae_mfe_analysis 中的 violinmode 參數控制顯示模式,預設為group模式,將勝敗手分群統計,overlay模式為全數統計。提琴圖hover過後能顯示數據的分位數資料,可快速觀察所有數據的統計分佈,方便設定停損停利點能參考分位數的數值。 除了先前介紹的mae_mfe,其他還有統計數值:

  • pdays_ratio:獲利交易日數/交易持有日數,中位數數值若大於0.5,代表多數交易持有期間都是獲利,操作起來更有彈性。若mfe高,但pdays_ratio低,代表若沒把握到衝高的少數時期,則會錯過理想報酬。

結論

是不是對波動分析更加瞭解了呢?一張圖表包山包海,完整分析出策略細節。

若想更深入瞭解MAE/MFE最大幅度分析法。除了國外資源,中文內容推薦藍月記事,其對這方面的策略體質觀察、優化有全方位的影片教學內容,作者對量化分析與交易心理有獨道見解,推薦大家前往學習。


為什麼策略優化容易讓 SL/TP 往 MAE/MFE 分佈圖左上移動

出處: https://www.maemfe.org/2020/05/why-the-sltp-moving-to-the-top-left-corner-of-MAEMFE-plot.html

今天來談一個在策略優化過程中,可能會遇到的一種情況

當我們在優化交易策略的參數的時候,有些人會把 Stoploss 和 Takeprofit 放進去一起優化。

這不是不行,如果你已經有一套方法去同時優化模型參數、SL/TP 那很好,但是如果你優化完,你發現每次到樣本外常常都掛掉的話,我會猜測可能 錯誤以為 MDD 變小是模型優化的「因」。

通常來說,許多人會使用「粒度大」、「交易間」的評估指標,例如最大連續虧損(MDD)做為策略優化的參考 ,希望盡量找到一個 MDD 小的參數。

如果此時伴隨著 SL 和 TP 下去優化,有時候會得到 SL 開始變小TP 開始變大,然後 MDD 變小的情況,很多人認為,這樣的情況代表「」,因為每一筆交易的潛在虧損更小、潛在獲利更大,整體的 MDD 更小,感覺應該是更穩健。

在一些條件考量之下,這樣的想法是對的,但如果你如果經常使用這方式,還是無法在外樣本得到好的結果的話,你要謹慎考慮其實你落入一個優化的陷阱。

當你使用粒度大的評估指標,什麼是粒度大呢?也就是可能因為越少的交易,就使得這個評估指標急遽的變差的,我們就叫做粒度大的評估指標。

最大連續虧損就可能因為少數幾個交易連續大幅度的損失,就造成 MDD 非常高,所以 MDD 某種程度可以看成是粒度較大的評估指標;其次是夏普率,夏普率可能會因為少數一些交易帶來大幅度的報酬/虧損,使得淨值曲線的標準差急遽升高,這也是一個粒度大的評估指標。

這些粒度大的評估指標,因為容易受到少數的交易影響,所以你在優化過程中,很難在一開始就針對這些少數交易對陣下藥,所以如果你能讓 MDD 持續的下降,通常伴隨的是一種「全局」調整。

你認為可能因為些微修改模型參數,就能突然改善那些影響 MDD 的少數交易嗎?其實很難,大多數時候,你會發現都是因為 SL/TP 的調整,讓 MDD 變小、讓夏普遍高。

所以,這個錯誤的以 MDD 為因的情況,可能是以這樣的一種情況在優化:

調整模型參數、訊號濾網 → 降低SL/提高TP → MDD變小

所以你以為你在優化參數、調整濾網,讓 MDD 變小,但其實你是在讓模型參數能讓你更加的能降低 SL 提高 TP,這樣才能「對全部交易有直接影響」而間接降低 MDD,所以 MDD 變小隻是一個結果。

這會有什麼問題呢?

當持續性的降低 SL 和提高 TP,你其實對於波動的忍耐度就更低,所以你如果參數還能賺錢,通常來說你只是在不斷地讓模型參數、訊號濾網維持一個程度的勝率,使得這個勝率下的 SL 能繼續縮小、TP 繼續提高。

通常維持勝率比較依賴進場的時機,當勝率無法維持,通常下一步就是調整出場時機,讓某一些少數交易能帶來大幅度的獲利,所以接下來會看見勝率衰減、平均報酬增加、平均虧損降低,然後繼續縮小 SL,提高 TP。

在這過程中,你的進場和出場已經被特別優化了,這時候你如果還是使用「交易間」的評估方式,你就根本看不到買賣訊號裡面到底有沒有配合波動進出。

到最後,就是過擬合。

很多人發現,**在這樣的優化過程中只要稍微考慮一下加減碼,就會讓優化變得順利一點,為什麼?**因為你等於把 SL/TP 過度優化的負擔分攤出去,然後你的加減碼某種程度其實反應的就是在不同市場波動水準下的調整,當然你在樣本外就會看到好像和樣本內有一咪咪相似的感覺。

但是這樣模型還是不能用,你還是會掛掉,如果可以用,你就會發現你要經常性的重新訓練和優化你的策略參數,然後還有一些人以為,這樣代表我在「適應」市場,其實他只是在脫褲子放屁的「適應波動」而已。

Maximum favorable excursion and Maximum adverse excursion calculation Toolkit

出處 : https://github.com/RainBoltz/pymfae

import matplotlib.pyplot as plt
import pandas as pd
import datetime as dt
import numpy as np
import plotly.graph_objs as go
import plotly.io as pio
import json


def get_mae(
    order_type: str,
    entry_time: dt.datetime or int,
    exit_time: dt.datetime or int,
    price_data: pd.DataFrame or pd.Series or np.array or list,
):
    """
    parameters:
        order_type: str, 做多或做空 ('long' 或 'short')
        entry_time: dt.datetime 或 int, 進場時間 (若有OHLC資料,則視為open進場)
        exit_time: dt.datetime 或 int, 出場時間 (若有OHLC資料,則視為close出場)
        price_data: pd.DataFrame 或 pd.Series, 以datetime為index的價格資料
                    np.array 或 list, 以陣列紀錄的價格資料
                    (DataFrame為OHLC資料;Series、Array或List則為Tick或Close資料)

    return:
        mae: float, 交易期間最大回徹
        mae_time: dt.datetime 或 int, MAE發生時間
    """
    mae = 0.0
    mae_time = None

    if type(price_data) == pd.DataFrame:
        data = price_data.loc[entry_time:exit_time]
        entry_price = data["open"].loc[entry_time]
        for index, values in data.iterrows():
            if order_type == "long":
                drawdown = values["low"] - entry_price
            elif order_type == "short":
                drawdown = entry_price - values["high"]
            if mae > drawdown:
                mae = drawdown
                mae_time = index

    elif type(price_data) == pd.Series:
        data = price_data.loc[entry_time:exit_time]
        entry_price = data.loc[entry_time]
        for index, values in enumerate(data):
            if order_type == "long":
                drawdown = values - entry_price
            elif order_type == "short":
                drawdown = entry_price - values
            if mae > drawdown:
                mae = drawdown
                mae_time = index

    elif type(price_data) == np.array or type(price_data) == list:
        entry_price = price_data[entry_time]
        for index, values in enumerate(price_data[entry_time : exit_time + 1]):
            if order_type == "long":
                drawdown = values - entry_price
            elif order_type == "short":
                drawdown = entry_price - values
            if mae > drawdown:
                mae = drawdown
                mae_time = entry_time + index

    return mae, mae_time


def get_mfe(
    order_type: str,
    entry_time: dt.datetime or int,
    exit_time: dt.datetime or int,
    price_data: pd.DataFrame or pd.Series or np.array or list,
):
    """
    parameters:
        order_type: str, 做多或做空 ('long' 或 'short')
        entry_time: dt.datetime 或 int, 進場時間 (若有OHLC資料,則視為open進場)
        exit_time: dt.datetime 或 int, 出場時間 (若有OHLC資料,則視為close出場)
        price_data: pd.DataFrame 或 pd.Series, 以datetime為index的價格資料
                    np.array 或 list, 以陣列紀錄的價格資料
                    (DataFrame為OHLC資料;Series、Array或List則為Tick或Close資料)

    return:
        mfe: float, 交易期間最大回徹
        mfe_time: dt.datetime 或 int, MAE發生時間
    """
    mfe = 0.0
    mfe_time = None

    if type(price_data) == pd.DataFrame:
        data = price_data.loc[entry_time:exit_time]
        entry_price = data["open"].loc[entry_time]
        for index, values in data.iterrows():
            if order_type == "long":
                profit = values["high"] - entry_price
            elif order_type == "short":
                profit = entry_price - values["low"]
            if mfe < profit:
                mfe = profit
                mfe_time = index

    elif type(price_data) == pd.Series:
        data = price_data.loc[entry_time:exit_time]
        entry_price = data.loc[entry_time]
        for index, values in enumerate(data):
            if order_type == "long":
                profit = values - entry_price
            elif order_type == "short":
                profit = entry_price - values
            if mfe < profit:
                mfe = profit
                mfe_time = index

    elif type(price_data) == np.array or type(price_data) == list:
        entry_price = price_data[entry_time]
        for index, values in enumerate(price_data[entry_time : exit_time + 1]):
            if order_type == "long":
                profit = values - entry_price
            elif order_type == "short":
                profit = entry_price - values
            if mfe < profit:
                mfe = profit
                mfe_time = entry_time + index

    return mfe, mfe_time


def mae_mfe_pair(
    order: list,
    price_data: pd.DataFrame or pd.Series or np.array or list,
    mae_first: bool = True,
):
    """
    parameters:
        order: list, 所有交易紀錄,交易紀錄為dict
                交易紀錄格式:
                {
                    'order_type': str,
                    'entry_time': dt.datetime 或 int,
                    'exit_time': dt.datetime 或 int
                }
        price_data: pd.DataFrame 或 pd.Series, 以datetime為index的價格資料
                    np.array 或 list, 以陣列紀錄的價格資料
                    (DataFrame為OHLC資料;Series、Array或List則為Tick或Close資料)
        mae_first: bool (預設為True), MFE是否出現在MAE之前

    return:
        results: list, 所有交易紀錄結果,交易紀錄結果為dict
                交易紀錄結果格式:
                {
                    'mae': float,
                    'mfe': float,
                    'mae_time': dt.datetime 或 int,
                    'mfe_time': dt.datetime 或 int
                }
    """
    results = []
    for this_order in order:
        mae, mae_time = get_mae(
            this_order["order_type"],
            this_order["entry_time"],
            this_order["exit_time"],
            price_data,
        )

        if mae_first:
            mfe, mfe_time = get_mfe(
                this_order["order_type"], this_order["entry_time"], mae_time, price_data
            )
        else:
            mfe, mfe_time = get_mfe(
                this_order["order_type"],
                this_order["entry_time"],
                this_order["exit_time"],
                price_data,
            )

        results.append(
            {"mae": mae, "mfe": mfe, "mae_time": mae_time, "mfe_time": mfe_time}
        )

    return results


def mae_mfe(orders, x_y_line=True, return_fig_ax=False):
    """
    parameters:
        orders: list, 所有交易的mae和mfe資料,資料必須放在dict裡面
            資料格式:
            {
                'mae': float,
                'mfe': float,
                ...
                (可以包含其他資料)
            }
        
        x_y_line: bool (預設為True), 是否繪製x=y的虛線
        return_fig_ax: bool (預設為False), 是否回傳matplotlib的繪圖元件

    return:
        (optional)
        fig: plotly的基本繪圖元件
    """
    fig = go.Figure()

    mae = []
    mfe = []
    for order in orders:
        mae.append(abs(order["mae"]))
        mfe.append(order["mfe"])
    fig.add_trace(
        go.Scatter(
            x=mae, y=mfe, mode="markers", name="Orders", marker=dict(color="red")
        )
    )

    if x_y_line:
        x_y_line_x = [0, max(max(mae), max(mfe))]
        x_y_line_y = [0, max(max(mae), max(mfe))]
        fig.add_trace(
            go.Scatter(
                x=x_y_line_x,
                y=x_y_line_y,
                mode="lines",
                name="x=y",
                line=dict(dash="dash", color="black", width=1),
            )
        )

    fig.update_layout(
        xaxis_title="MAE", yaxis_title="MFE", title="MAE vs MFE", legend=dict(title="")
    )

    if return_fig_ax:
        return fig
    else:
        pio.show(fig)


if __name__ == "__main__":

    # Define your price data as a pandas DataFrame or Series, or as a numpy array or list
    price_data = pd.DataFrame(
        {
            "open": [10, 12, 15, 13, 14],
            "high": [13, 15, 17, 16, 16],
            "low": [9, 11, 14, 12, 12],
            "close": [12, 14, 16, 14, 15],
        },
        index=[
            dt.datetime(2021, 1, 1, 9, 0),
            dt.datetime(2021, 1, 2, 9, 0),
            dt.datetime(2021, 1, 3, 9, 0),
            dt.datetime(2021, 1, 4, 9, 0),
            dt.datetime(2021, 1, 5, 9, 0),
        ],
    )

    # Define your orders as a list of dictionaries, with each dictionary representing an order
    orders = [
        {
            "order_type": "long",
            "entry_time": dt.datetime(2021, 1, 1, 9, 0),
            "exit_time": dt.datetime(2021, 1, 2, 9, 0),
        },
        {
            "order_type": "short",
            "entry_time": dt.datetime(2021, 1, 2, 9, 0),
            "exit_time": dt.datetime(2021, 1, 3, 9, 0),
        },
        {
            "order_type": "long",
            "entry_time": dt.datetime(2021, 1, 3, 9, 0),
            "exit_time": dt.datetime(2021, 1, 4, 9, 0),
        },
        {
            "order_type": "short",
            "entry_time": dt.datetime(2021, 1, 4, 9, 0),
            "exit_time": dt.datetime(2021, 1, 5, 9, 0),
        },
    ]

    print(price_data.to_markdown(), "\n")
    print(json.dumps(orders, indent=4, default=str))

    # Call the mae_mfe_pair function with your orders and price data
    results = mae_mfe_pair(orders, price_data)

    # The results will be a list of dictionaries, with each dictionary representing the MAE/MFE for an order
    print(json.dumps(results, indent=4, default=str))

    # mae_mfe(results)
|                     |   open |   high |   low |   close |
|:--------------------|-------:|-------:|------:|--------:|
| 2021-01-01 09:00:00 |     10 |     13 |     9 |      12 |
| 2021-01-02 09:00:00 |     12 |     15 |    11 |      14 |
| 2021-01-03 09:00:00 |     15 |     17 |    14 |      16 |
| 2021-01-04 09:00:00 |     13 |     16 |    12 |      14 |
| 2021-01-05 09:00:00 |     14 |     16 |    12 |      15 | 

[
    {
        "order_type": "long",
        "entry_time": "2021-01-01 09:00:00",
        "exit_time": "2021-01-02 09:00:00"
    },
    {
        "order_type": "short",
        "entry_time": "2021-01-02 09:00:00",
        "exit_time": "2021-01-03 09:00:00"
    },
    {
        "order_type": "long",
        "entry_time": "2021-01-03 09:00:00",
        "exit_time": "2021-01-04 09:00:00"
    },
    {
        "order_type": "short",
        "entry_time": "2021-01-04 09:00:00",
        "exit_time": "2021-01-05 09:00:00"
    }
]
[
    {
        "mae": "-1",    
        "mfe": "3",
        "mae_time": "2021-01-01 09:00:00",
        "mfe_time": "2021-01-01 09:00:00"
    },
    {
        "mae": "-5",
        "mfe": "1",
        "mae_time": "2021-01-03 09:00:00",
        "mfe_time": "2021-01-02 09:00:00"
    },
    {
        "mae": "-3",
        "mfe": "2",
        "mae_time": "2021-01-04 09:00:00",
        "mfe_time": "2021-01-03 09:00:00"
    },
    {
        "mae": "-3",
        "mfe": "1",
        "mae_time": "2021-01-04 09:00:00",
        "mfe_time": "2021-01-04 09:00:00"
    }
]

# 解說: "order_type": "long", "entry_time": "2021-01-01 09:00:00", "exit_time": "2021-01-02 09:00:00" 
# 2021-1-1 以open價為基準是10, 2021-1-1 low:9 所以mae: -1, 2021-1-1 high:13 所以  
"mae": "-1",    
"mfe": "3",
"mae_time": "2021-01-01 09:00:00",
"mfe_time": "2021-01-01 09:00:00"
  • 策策略裡停損的標準是採用mdd還是mae?
    • mae, mdd跟移動出場比較有關
  • B_mfe
    • B_mfe是mae之前的mfe,做多的話,也就是你吃到最大下跌前的漲幅,一般會用mae決定停損位置,mfe決定停利,若你的B_mfe蠻高的,有比較高的機率在停損前停利,這是理想的狀況。
  • G_mfe
    • G_mfe是全域的mfe,最大漲幅可能出現在mae之後,若你的策略B_mfe很小,G_mfe較大,代表你的策略容易先吃跌再上衝,停損不能設太緊,不然會被洗掉,通常這是波動大策略會有的現象,應對策略可以考慮延遲進場,降低成本,這會延伸到edge ratio對mfe/mae的時序分析,看哪時候進場的mfe/mae最大。
  • 策略若個股勝率低於40%,建議別再自選,整個策略買,自己再選容易買到跌的,但策略整體卻是漲。
    • 我是習慣加條件把策略縮到5-10檔,集火拚高報酬,偏愛動能與高成長。每個月換股一次讓他去跑。 進出場點優化用海龜法則的優勢比率分析、停損停利用mae mfe分析。

計算出 mae bmfe gmfe

import pandas as pd


def get_metrics(prices):
    base_price = prices[0]
    min_price = min(prices)
    max_price = max(prices[1:])

    max_price_before_min = (
        max(prices[: prices.index(min_price)])
        if min_price is not None and prices[: prices.index(min_price)] != []
        else None
    )

    bmfe = (
        ((max_price_before_min - base_price) / base_price) * 100
        if max_price_before_min is not None and base_price < max_price_before_min
        else 0
    )

    gmfe = (
        ((max_price - base_price) / base_price) * 100
        if max_price is not None and max_price > base_price
        else 0
    )

    mae = (
        (min_price - base_price) / base_price * 100
        if min_price is not None and min_price < base_price
        else 0
    )

    return mae, bmfe, gmfe


def get_max_drawdown(prices):
    df = pd.DataFrame(prices, columns=["Close"])
    df["Max"] = df["Close"].cummax()
    df["Drawdown"] = df["Close"] / df["Max"] - 1
    df["Max Drawdown"] = df["Drawdown"].cummin()
    mdd = df["Max Drawdown"].min()
    return mdd * 100


if __name__ == "__main__":
    prices = [
        12.65,
        12.8,
        12.6,
        12.6,
        12.6,
        12.6,
        12.85,
        14.55,
        15.0,
        13.65,
        14.0,
        13.8,
        13.85,
        14.3,
        14.2,
        14.9,
        15.7,
        15.8,
        15.4,
        15.5,
        15.25,
        15.15,
        15.0,
        15.9,
    ]

    mae, bmfe, gmfe = get_metrics(prices)
    print(f"MAE: {mae:.5f}%")
    print(f"BMFE: {bmfe:.5f}%")
    print(f"GMFE: {gmfe:.5f}%")
    print(f"MDD: {get_max_drawdown(prices):.5f}%")
import pandas as pd

class PriceMetrics:
    def __init__(self, prices):
        self.prices = prices
        self.base_price = prices[0]
        self.min_price = min(prices)
        self.max_price = max(prices[1:])
        self.max_price_before_min = self._get_max_price_before_min()
        self.mae = self._calculate_mae()
        self.bmfe = self._calculate_bmfe()
        self.gmfe = self._calculate_gmfe()
        self.edge_ratio = self._calculate_edge_ratio()

    def _get_max_price_before_min(self):
        if self.min_price is None or self.prices[:self.prices.index(self.min_price)] == []:
            return None
        return max(self.prices[:self.prices.index(self.min_price)])

    def _calculate_mae(self):
        if self.min_price is None or self.min_price >= self.base_price:
            return 0
        return (self.min_price - self.base_price) / self.base_price * 100

    def _calculate_bmfe(self):
        if self.max_price_before_min is None or self.base_price >= self.max_price_before_min:
            return 0
        return ((self.max_price_before_min - self.base_price) / self.base_price) * 100

    def _calculate_gmfe(self):
        if self.max_price is None or self.max_price <= self.base_price:
            return 0
        return ((self.max_price - self.base_price) / self.base_price) * 100

    def _calculate_edge_ratio(self):
        if self.mae == 0:
            return 0
        return ((self.gmfe - self.mae) / abs(self.mae)) * 100

    def get_max_drawdown(self):
        df = pd.DataFrame(self.prices, columns=["Close"])
        df["Max"] = df["Close"].cummax()
        df["Drawdown"] = df["Close"] / df["Max"] - 1
        df["Max Drawdown"] = df["Drawdown"].cummin()
        mdd = df["Max Drawdown"].min()
        return mdd * 100

if __name__ == "__main__":
    #prices = [100, 90, 80, 70, 60, 50, 40, 30, 20, 10]
    prices = [10, 20, 15, 25, 18, 30, 12]
    metrics = PriceMetrics(prices)
    print(metrics.mae)
    print(metrics.bmfe)
    print(metrics.gmfe)
    print(metrics.edge_ratio)
    print(metrics.get_max_drawdown())

MAE 跟 MDD 差異

  • 舉例:若持有期間從原本賺10%變到虧損5%,MAE絕對值為5%,代表持有期間最大虧損 5%,MDD絕對值則是15%

我談的 MAE/MFE 與〈海龜投資法則〉的差異

出處:https://www.maemfe.org/2020/01/analyzing-mae-mfe-from-the-time-axis.html

我談的 MAE/MFE 與〈海龜投資法則〉的差異

我在 PTT 和人分享 MAE/MFE 的概念的時候,一位網友就和我發想:

我其實非常高興,他在看完我的影片之後,就自發性地想到這個點

也就是說不從價格面(Y 軸)分析 MAE, MFE ,而是從(X 軸)時間面來分析

沒錯,從時間面來分析,正巧就是**《海龜投資法則》**一書中

談到的 優勢比率(Edge Ratio)

也有人稱呼 Edge Ratio 叫做 e-ratio 或 E 比率

優勢比率

在書中作者提出一種評估策略好壞的參考,就是從進場之後 t 個時間單位後的 MAE, MFE

透過計算 MFE 除以 MAE 的值,可以得到 e-ratio

](https://imgur.com/APLnQoJ.png)

img

除此之外,我們還可以計算進場之後不同時間點的 e-ratio ,簡寫成 e(時間) 的形式

你會發現,在我錄的許多影片中,我幾乎沒有提到這個時間面的處理方式

原因是時間面的處理方式,是比較進階的主題,外匯市場的波動在一個小尺度比較固定,但交易成本高

但是如果你想要更仔細去挖掘你的策略是否真的給你帶來良好的體質,或是你的交易策略本身交易次數不夠多

尤其是在更高的時間尺度(例如 H12, Daily)使用時間面的分析是很重要的

除此之外,容易受到時間點的影響的標的(例如股票、原物料)

進場時間與進場的時機,為你後續帶來的時間面的 MAE, MFE 就相對比較重要

最後的碎碎唸

最後我想講的事情是,我的影片的目的很大程度就是這樣

我要啟發你思考,你其實見解不會輸那些大師,你看在 PTT 很隨意

就有人能從 MAE, MFE 基本觀念,想到海龜投資法則大師的分析想法

所以說,我今天不是在給你一個確定的結論,我是要你更多的去思考

你得到的結論可能會對你有無比的幫助,甚至不輸許多高手

很多時候就是欠缺自信,大家要多加相信自己!努力去研究!不要鑽漏洞!

腳踏實地的修改交易策略和努力分析,我在這和你討論,不是很好嗎~

彈性進出場的判斷 | 優勢比率應用

出處: https://www.finlab.tw/edge-ratio-follow-application/

當你開發完策略,也跑完統計清單,有沒有碰過一種狀況是策略換股週期在月初每月換股,但現在已經月中,你在猶豫適不適閤中途進場? 你一定想過若點位和日期不同,雖然是同一檔標的,但不同價位所面臨的風險完全不一樣,可能策略回測賺錢,但你中途進價太高,導致最後是虧損的局面。

或是你害怕中途進場買高,結果策略一路走高,你只能看著他一路飆,錯過補票機會。 雖然保守一點來看,其實這也沒什麼不好,少賺總比賠錢好,想貼合回測曲線,下次換股日再注意也是選項,但有沒有辦法讓我們判斷策略的進出場彈性,做更積極的決策?

優勢比率定義

要如何判斷策略適不適合補票(中途進場或加碼)?可以藉由海龜交易法則的 Edge Ratio (優勢比率來判斷)。 優勢比率為平均 GMFE (策略每筆交易紀錄的最大有利幅度) 除以平均 MAE (策略每筆交易紀錄的最大不利幅度)。這可以藉此評估進場優勢,一個隨機性的訊號大致上會帶來相等的 MFE 與 MAE。

若大於1,代表存在正優勢,潛在最大獲利空間比最大虧損多,在持有過程中保有優勢可以中途停利或做其他操作,也就是策略的容錯率較高。反之則為劣勢,可能要抗衡較多的虧損狀態。

優勢比率時序分析

我們加上時序分析,判斷策略每筆歷史交易持有過程 n 天內的優勢比率變化,看看隨著持有時間變長,優勢比率是不是會走高?通常一個好的趨勢策略,都會逐步拉開優勢空間。

如何使用 FinLab Package 顯示策略的優勢比率?

回測函數sim 裡面的參數「mae_mfe_window」控制「優勢比率時序圖」的時間長度,設定40就是看40天的變化,為了加快回測運算,此參數預設為None,如果要顯示優勢比率,且既一定要自己設定「mae_mfe_window」數值。 一般來說若是月週期 (20交易日) 的策略,我都會拉長一點到40,看策略有沒有可能20天後的edge_ratio持續走升,若是此情況,可以著墨策略延後出場,獲取更多報酬的可能性。 sim會回傳report物件,使用report物件內的display_mae_mfe_analysis()方法即可顯示「波動分析圖」。想知道「波動分析圖」更多應用可參考此篇文章

from finlab.backtest import sim

report=sim(position=position, mae_mfe_window=40)
report.display_mae_mfe_analysis()

回傳圖表的最上排第二張子圖即是「優勢比率時序圖」。

newplot 1 1「波動分析圖

分析案例

舉幾個FinLab策略的時序圖來示範「優勢比率時序圖」如何分析 ?

營收動能瘋狗策略

edge ratio 1

優勢比率開高一路走升,優勢空間隨著時間放大,擁有不錯的趨勢策略特質,有較高的近場彈性,但接近第20天左右(下次營收截止日換股)有高峰,這時就不建議中途進場,容易在高點套住,應等下期緩股訊號出現。 策略連結

投信大哥跟屁蟲策略

edge ratio 2

三天決勝負的短線策略。 優勢開高後一路走跌,優勢空間隨著時間快速縮小,越慢進場的局面越不利。 極度不適合延遲進場,沒跟到第一天就別跟了。 投信買賣超這個因子對短線較有影響力,過去市場應有不少人在投信短期大買後跟單,導致此現象,但這項催化劑也退的很快,長期走勢的影響力就不大,容易被其他變數幹擾,較不適合當中長期因子。 策略連結

藏獒策略

edge ratio 3

類似營收動能瘋狗策略,但不強調營收創新高,比較多轉機股條件。 優勢比率在第五天創出小高峰後,會有一段明顯回撤,也就是延遲第8天進場的話,甚至有機會買的比第一天成本比,過了這個小低谷後,優勢空間開始走出大波段。 策略連結

結論

你的策略有什麼樣子的「優勢比率時序圖」呢?趕緊用FinLab Package 來實做看看,會夠瞭解你策略的細節喔!

三心法順勢操作 陳族元10年資產翻10倍

https://www.businesstoday.com.tw/article/category/80401/post/201805300009/

覆巢之下無完卵,再好的股票,當遇到大環境趨勢反轉向下,一樣可以摔得體無完膚。」陳族元若有所思地說。

他認為,投資市場的贏家有兩種人,一種是基本面派,有機會長抱、賺大波段行情,卻也容易執著自己看好基本面的個股,不願意相信自己看錯,會敗在停損太慢。第二種是主力派,看籌碼、看技術面,但對買的股票不見得了解,也可能敗在誤判公司經營實績。

心法1》研判臺股多空 美元指數、公債殖利率、升息進度

原本篤信基本面派的陳族元,為了精進投資技巧,集兩者的優點,藉由建立投資SOP,從而也盡可能地減少判斷盲點。他採取的作法是,首先觀察大環境趨勢,包括外資、主力籌碼變化,研判臺股大盤多空轉折;再從趨勢產業,聚焦基本面轉好,技術面與籌碼面轉強的個股,並以此作為進出場的依據。而無論是看大盤或看個股、看基本面或技術面,關鍵,都是判斷多空趨勢轉折是否即將發生。

陳族元分析,「掌握轉折的順勢操作法」一共有三大心法。首先,是從美元指數、美國十年期公債殖利率,以及聯準會(Fed)公佈的升息進度變化等,研判臺股大盤多空走勢。

簡單地說,這三者都攸關臺股資金水位,弱美元、低利率、聯準會採寬鬆政策,相對有利臺股資金,透過這樣的基本判斷,可以初步擬定臺股資金部位的控管。

研判臺股大盤多空走勢時,確實可以考慮美元指數、美國十年期公債收益率以及聯邦儲備系統(Fed)的升息進度等因素。這些因素可以影響全球資本流動、市場信心和企業盈利等方面,從而對股市產生影響。

以下是一些常考慮的因素:

美元指數: 臺股通常受到全球資本流動的影響,而美元是國際貿易和金融活動中的主要貨幣。美元指數的走勢可以反映出美元相對其他主要貨幣的強弱,對出口導向型經濟的臺灣來說,美元的變化可能對企業盈利和出口業績有一定的影響。

美國十年期公債收益率: 長期利率的變動可以影響企業的融資成本和投資決策,也可以影響投資者對股票相對於債券的偏好。如果十年期公債收益率上升,可能導致投資者更傾向於債券而非股票,從而對臺股形成一定的壓力。

聯邦儲備系統的升息進度: 如果美聯儲加息,這可能導致全球資本流動的變化,也可能影響到全球的資產價格。升息可能對股票市場形成一定的負面影響,因為投資者可能更傾向於尋求相對較安全的資產。

宏觀經濟指標: 關注臺灣和全球的宏觀經濟指標,例如GDP增長率、出口資料、失業率等,可以提供對整體經濟環境的瞭解,從而影響股市表現。

地緣政治風險: 全球地緣政治緊張局勢可能對股市產生不確定性。關注國際事務,特別是與臺灣相關的地緣政治動態,對於預測市場可能的波動具有重要意義。

在進行多空走勢的研判時,綜合考慮這些因素,並密切關注相關的新聞和資料,可以幫助制定更全面的投資策略。需要注意的是,股市受多種因素影響,且市場具有一定的不確定性,因此投資決策應該謹慎,並根據個人的風險偏好和投資目標做出相應的調整。

心法2》觀察外資籌碼 期貨多空口數、買賣超金額、臺指期

其次,在籌碼觀察方面,則會從外資期貨淨多單與淨空單的口數、外資買賣超金額變化、臺指期佈局情況,研判大盤多空方向。例如二○一五年四月二十七日,臺股前一次攻上萬點時,外資淨多單卻不到萬口,隔日指數創高,淨多單更大減至五千七百多口,代表外資心態保守,隨後五月轉成淨空單,僅四天就超過一萬口,即使當時外資現貨買超,若忽略期貨警訊,恐怕難逃當時萬點下修的命運。

至於今年臺股大盤怎麼看?陳族元直言,綜觀上述指標做觀察,相較於去年大多頭行情明確,今年在操作上,宜更謹慎看待,積極留意多空轉折訊號。他以前面提到的幾項指標分析,近來美國十年期公債殖利率屢過三%,債券殖利率走高,加上美元轉強,熱錢從新興市場回到美國,再加上聯準會採緊縮貨幣姿態,均不利多頭。

事實上,早在今年一月間,他就看見當時外資淨多單不增反減,一月三十日也終止連日來的現貨買超,加上主力籌碼、投資法人也出現連續性賣盤,讓他開始佈局空單,後來大盤果然下修至一○一八九點的波段低點,空單佈局讓他賺了近千點的行情。

後來,雖然臺股開始跌深反彈,陳族元也在三月中旬作多被動元件,但美國總統川普對中國進口加徵關稅,兩國貿易戰的不確定性,以及國際油價大漲,使得通膨疑慮升高,未來聯準會升息腳步加快,讓投資部位不小的他,操作策略猶如法人,雖然現貨持股有九成,但期貨避險空單也不在少數。對於不作空的投資人,他建議目前持股最好不要超過五成,以因應臺股後市的盤勢變化。

心法3》精選趨勢個股 鎖定基本面成長、擁轉機題材標的

確定大盤方向後,就是第三心法,精選個股。對此,陳族元會鎖定趨勢產業,挑基本面出現成長或轉機題材的公司,進而從籌碼、K線、價量、形態等技術面表現,來確認轉折與趨勢,作為進出場的依據,尤其在籌碼與基本面獲利數字的計算方面,陳族元也都會製作Excel分析表,當作進出場的準則。

陳族元以先前操作的飆股撼訊為例,他在去年十月十一日當撼訊股價突破整理平臺、在三十八元附近買進時,也沒想到今年四月會大漲到波段高點四二○元。陳族元說,去年八月時,他就看好乙太幣飆漲的商機,經過基本面研究發現,撼訊是相關挖礦商機的重點供貨廠商,且股本小,獲利貢獻度最高,是賺機會財的最佳口袋名單。

他分析,一七年五月起,撼訊連續六個月營收年增率成長,而當營收超過一定規模後,獲利跳升的速度開始加快,從六月單月每股EPS一元、七月二元多就可看出端倪;他推估,當公司營收超過三億元,每多一千萬元就可以貢獻每股純益○.一元,因此只要公司一公佈營收,就能概算出獲利,當他算出撼訊今年第一季單季就賺四元至五元,更是讓他見獵心喜,因為有了乙太幣風潮的趨勢,加上基本面獲利跳升,再搭技術面轉強與主力籌碼追進,更能增加投資勝率。

擅長追蹤籌碼的陳族元說,撼訊股價最剽悍的一八○元漲到四二○元波段,「就是典型的噴出行情,是市場老主力的傑作。」他從追蹤券商分點籌碼推測,追捧大咖多為市場資金雄厚的主力,他笑著舉例,「不難發現,富隆證券陸續有大單,市場就揣測這是出自老主力『阿不拉』之手。」

只是,在撼訊大漲的過程中,他沒有一路抱到四月最高點四二○元,而是在股價八○元、一五○元附近等陸續出脫持股。回頭來看,雖然放掉了最後的大波段,但因為從基本面分析精準抓到「向上轉折點」,也讓他在前後不到半年的時間仍有超過五倍獲利,就獲利倍數來看,不輸由市場主力主導的最後末升段。

換個角度思考,當個股漲勢由主力主導,一旦反轉,跌勢往往也更加迅猛,殺得散戶措手不及。相對之下,若能「由基本面提早掌握向上轉折買點」,則可讓自己不必為了賺飽賺足,而承擔「與主力互鬥」的風險。

另方面,之所以提早出場,也是因為陳族元發現了新的轉折向上獲利機會。「你會發現,三、四月矽晶圓族群股價拉高,主力籌碼一直賣,被動元件漲這麼多,卻一直有人買。進一步研究基本面,你就會發現未來股價仍很有戲。」

陳族元說,他在華新科股價九十幾元陸續大單佈局,抱到一七○元附近,又發現國巨更值得投資,轉而在五、六百元附近加碼國巨,尤其從資金追逐的角度來看今年三月以來的漲勢,不但主力籌碼大增,千張以上大戶持股也在一定的水準,後續果然一度飆高到一○六五元,惟近期散戶籌碼增加,主力籌碼出現調節,宜留意股價漲多整理的震盪。

[陳族元 ]

展望後市 看好被動元件、矽晶圓、金融

展望未來,陳族元看好的標的有被動元件的國巨、自動化設備廠全球傳動、矽晶圓的合晶與中美晶,以及受惠升息環境的金融股新光金等。而受到原物料漲勢激勵,他也看好大成鋼擁有六個月低價庫存的優勢,今年EPS上看五元,至於華新持有華新科,現金殖利率近四%,法人預估今年EPS有三元多,也是值得留意的標的。

無論是從資金大環境判斷大盤轉折,或者是從基本面提早抓住向上轉折點,都是為了能夠降低「系統性翻盤」、「與主力互鬥」的風險。也就是仰賴這樣的投資準備,讓陳族元即使面臨臺股高檔震盪,也能持盈保泰,在茫茫股海中戰無不勝。

JUMP

import pandas as pd
import warnings
import numpy as np
import vectorbt as vbt
import talib
from vectorbt.portfolio.enums import NoOrder
from vectorbt.portfolio import nb
from numba import njit
from loguru import logger

# Suppress warnings
warnings.filterwarnings("ignore")


def initialize_data(data):
    """
    Convert DataFrame columns to NumPy arrays for performance.

    Parameters:
        data (pd.DataFrame): The trading data.

    Returns:
        tuple: Contains various NumPy arrays extracted from the DataFrame.
    """
    is_thursday = data["Is_Thursday"].to_numpy()
    is_open_time = data["Is_Open_Time"].to_numpy()
    is_close_time = data["Is_Close_Time"].to_numpy()
    dates = data.index.strftime("%Y-%m-%d %H:%M:%S").tolist()
    timestamps = (pd.to_datetime(data.index).astype(int) // 10**9).tolist()
    open_prices = data["open"].to_numpy()
    close_prices = data["close"].to_numpy()
    high_prices = data["high"].to_numpy()
    low_prices = data["low"].to_numpy()
    long_conditions = data["long_condition"].to_numpy()
    short_conditions = data["short_condition"].to_numpy()
    long_atr_exit = data["long_atr_exit_value"].to_numpy()
    short_atr_exit = data["short_atr_exit_value"].to_numpy()
    atr_values = data["ATR"].to_numpy()

    return (
        is_thursday,
        is_open_time,
        is_close_time,
        dates,
        timestamps,
        open_prices,
        close_prices,
        high_prices,
        low_prices,
        long_conditions,
        short_conditions,
        long_atr_exit,
        short_atr_exit,
        atr_values,
    )


@njit
def execute_order(
    context,
    long_entry,
    short_entry,
    open_prices,
    high_prices,
    low_prices,
    entry_timestamps,
    trading_dates,
    trading_timestamps,
    is_thursday_flags,
    market_open_flags,
    market_close_flags,
    long_atr_sl,
    short_atr_sl,
    atr_vals,
    highest_since_entry,
    lowest_since_entry,
    max_high_M,
    min_low_M,
    check_interval_M,
    trading_mode,
):
    """
    Numba-compiled function to execute trading orders based on conditions.

    Parameters:
        context: Context object containing current state.
        ... (other parameters as in original function)

    Returns:
        Order action or NoOrder.
    """
    # Extract current data
    is_long = long_entry[context.i]
    is_short = short_entry[context.i]
    current_close = context.close[context.i, context.col]
    current_open = open_prices[context.i]
    current_high = high_prices[context.i]
    current_low = low_prices[context.i]
    current_date = trading_dates[context.i]
    current_timestamp = trading_timestamps[context.i]
    is_thursday = is_thursday_flags[context.i]
    is_market_open = market_open_flags[context.i]
    is_market_close = market_close_flags[context.i]
    current_position = context.position_now
    long_sl = long_atr_sl[context.i]
    short_sl = short_atr_sl[context.i]
    atr = atr_vals[context.i]

    if check_interval_M > 0:
        recent_max_high = max_high_M[context.i]
        recent_min_low = min_low_M[context.i]

    time_held = 0
    if entry_timestamps[0] > 0:
        time_held = int(current_timestamp - entry_timestamps[0])

    # Entry Logic
    if trading_mode in (0, 2):  # Long or Both
        if current_position == 0 and is_long and is_market_open:
            entry_timestamps[0] = current_timestamp
            highest_since_entry[0] = current_high
            return nb.order_nb(price=current_open, size=1)  # Enter Long

    if trading_mode in (1, 2):  # Short or Both
        if current_position == 0 and is_short and is_market_open:
            entry_timestamps[0] = current_timestamp
            lowest_since_entry[0] = current_low
            return nb.order_nb(price=current_open, size=-1)  # Enter Short

    # Long Position Management
    if trading_mode in (0, 2) and current_position > 0:
        highest_since_entry[0] = max(highest_since_entry[0], current_high)

        # ATR Stop Loss
        if current_close < long_sl:
            entry_timestamps[0] = 0
            highest_since_entry[0] = 0
            return nb.order_nb(price=current_close, size=-current_position)

        # Interval Check for Exiting
        if time_held >= (check_interval_M * 60) and check_interval_M > 0:
            if highest_since_entry[0] < recent_max_high:
                entry_timestamps[0] = 0
                highest_since_entry[0] = 0
                return nb.order_nb(price=current_close, size=-current_position)

    # Short Position Management
    if trading_mode in (1, 2) and current_position < 0:
        lowest_since_entry[0] = min(lowest_since_entry[0], current_low)

        # ATR Stop Loss
        if current_close > short_sl:
            entry_timestamps[0] = 0
            lowest_since_entry[0] = 0
            return nb.order_nb(price=current_close, size=-current_position)

        # Interval Check for Exiting
        if time_held >= (check_interval_M * 60) and check_interval_M > 0:
            if lowest_since_entry[0] > recent_min_low:
                entry_timestamps[0] = 0
                lowest_since_entry[0] = 0
                return nb.order_nb(price=current_close, size=-current_position)

    return NoOrder


def compute_open_price_change(df, open_time, close_time):
    """
    Calculate the percentage change in open prices compared to the previous close.

    Parameters:
        df (pd.DataFrame): The trading data.
        open_time (str): The opening time in "HH:MM:SS" format.
        close_time (str): The closing time in "HH:MM:SS" format.

    Returns:
        pd.DataFrame: DataFrame with an additional 'open_price_delta' column.
    """
    df_copy = df.copy()
    df_copy["open_price_delta"] = 0.0
    dates = df.index.strftime("%Y-%m-%d").tolist()
    unique_dates = sorted(set(dates))
    previous_date = unique_dates[0]

    for current_date in unique_dates[1:]:
        open_datetime = f"{current_date} {open_time}"
        close_datetime = f"{previous_date} {close_time}"
        if open_datetime not in df.index or close_datetime not in df.index:
            previous_date = current_date
            continue
        open_price = df.loc[open_datetime, "open"]
        close_price = df.loc[close_datetime, "close"]
        df_copy.loc[open_datetime, "open_price_delta"] = (open_price / close_price) - 1
        previous_date = current_date

    return df_copy


def calculate_atr(data, atr_period=14):
    """
    Calculate True Range (TR) and Average True Range (ATR).

    Parameters:
        data (pd.DataFrame): The trading data.
        atr_period (int): The period for ATR calculation.

    Returns:
        pd.DataFrame: DataFrame with additional 'TR' and 'ATR' columns.
    """
    data["TR"] = talib.TRANGE(data["high"], data["low"], data["close"])
    data["ATR"] = talib.EMA(data["TR"], timeperiod=atr_period)
    return data


def prepare_exit_levels(data, atr_multiplier):
    """
    Calculate ATR-based exit levels for long and short positions.

    Parameters:
        data (pd.DataFrame): The trading data with ATR calculated.
        atr_multiplier (float): The multiplier for ATR to set exit levels.

    Returns:
        pd.DataFrame: DataFrame with 'long_atr_exit_value' and 'short_atr_exit_value' columns.
    """
    data["long_atr_exit_value"] = data["high"].rolling(window=2).max() - (
        data["ATR"] * atr_multiplier
    )
    data["short_atr_exit_value"] = data["low"].rolling(window=2).min() + (
        data["ATR"] * atr_multiplier
    )
    return data


def load_and_process_data(filepath, start_date="2017-05-16"):
    """
    Load trading data from a CSV file and preprocess it.

    Parameters:
        filepath (str): Path to the CSV file.
        start_date (str): The starting date for the data.

    Returns:
        pd.DataFrame: Preprocessed trading data.
    """
    df = pd.read_csv(filepath)
    df["DateTime"] = pd.to_datetime(
        df["Date"] + " " + df["Time"], format="%Y/%m/%d %H:%M:%S"
    )
    df = df.drop(columns=["Date", "Time"])
    df.set_index("DateTime", inplace=True)
    df = df.rename(
        columns={
            "Close": "close",
            "Open": "open",
            "High": "high",
            "Low": "low",
            "TotalVolume": "volume",
        }
    )
    df = df[df.index >= pd.Timestamp(start_date)]
    return df


def main():
    # === Parameter Configuration ===
    # Toggle this to switch between different parameter sets
    use_first_set = True

    if use_first_set:
        # Parameter Set 1
        close_time = "13:24:00"
        open_time = "00:00:00"
        long_threshold = 0.001
        short_threshold = -0.01
        atr_length = 86
        atr_multiplier = 6.3
        M_values = [1260]  # Example: [1260]
        # M_values = range(100, 1501, 20)  # M 參數範圍
        trading_mode = 2  # 0: Long only, 1: Short only, 2: Both
    else:
        # Parameter Set 2
        close_time = "13:30:00"
        open_time = "00:46:00"
        long_threshold = 0.003
        short_threshold = -0.01
        atr_length = 82
        atr_multiplier = 9.5
        M_values = [-1]  # Example: [3700] or [-1]
        trading_mode = 1  # 0: Long only, 1: Short only, 2: Both

    # === Data Loading and Preprocessing ===
    data_filepath = "./TWF.TXF-HOT-Minute-Trade.txt"
    raw_data = load_and_process_data(data_filepath)
    data_with_change = compute_open_price_change(raw_data, open_time, close_time)
    data_with_change["long_condition"] = (
        data_with_change["open_price_delta"] > long_threshold
    )
    data_with_change["short_condition"] = (
        data_with_change["open_price_delta"] < short_threshold
    )
    data_with_change["Is_Thursday"] = data_with_change.index.weekday == 3
    data_with_change["Is_Open_Time"] = (
        data_with_change.index.time == pd.Timestamp(open_time).time()
    )
    data_with_change["Is_Close_Time"] = (
        data_with_change.index.time == pd.Timestamp(close_time).time()
    )
    data_with_atr = calculate_atr(data_with_change, atr_period=atr_length)
    data_with_exits = prepare_exit_levels(data_with_atr, atr_multiplier)

    # === Initialize Data for Numba ===
    (
        is_thursday,
        is_open_time,
        is_close_time,
        dates,
        timestamps,
        open_prices,
        close_prices,
        high_prices,
        low_prices,
        long_conditions,
        short_conditions,
        long_atr_exit,
        short_atr_exit,
        atr_values,
    ) = initialize_data(data_with_exits)

    # Initialize variables for tracking entries
    entry_timestamps = np.zeros(1, dtype=np.int64)
    highest_price_since_entry = np.zeros(1)
    lowest_price_since_entry = np.zeros(1)

    # === Optimization Loop ===
    best_M = 0
    best_return = -np.inf

    for check_interval_M in M_values:
        try:
            if check_interval_M > 0:
                max_high_M = (
                    data_with_exits["high"]
                    .rolling(window=check_interval_M)
                    .max()
                    .to_numpy()
                )
                min_low_M = (
                    data_with_exits["low"]
                    .rolling(window=check_interval_M)
                    .min()
                    .to_numpy()
                )
            else:
                max_high_M = np.empty(len(data_with_exits))
                min_low_M = np.empty(len(data_with_exits))

            # Create Portfolio using VectorBT
            portfolio = vbt.Portfolio.from_order_func(
                data_with_exits["close"],
                execute_order,
                long_conditions,
                short_conditions,
                open_prices,
                high_prices,
                low_prices,
                entry_timestamps,
                dates,
                timestamps,
                is_thursday,
                is_open_time,
                is_close_time,
                long_atr_exit,
                short_atr_exit,
                atr_values,
                highest_price_since_entry,
                lowest_price_since_entry,
                max_high_M,
                min_low_M,
                check_interval_M,
                trading_mode,
                init_cash=1_000_000,
            )

            # Display Orders
            print(
                portfolio.orders.records_readable.to_markdown(
                    floatfmt=".5f", tablefmt="heavy_grid"
                )
            )

            # Display Portfolio Statistics
            print(portfolio.stats())

            # Calculate Total Return
            total_return = portfolio.total_return()
            if total_return > best_return:
                best_M = check_interval_M
                best_return = total_return

            print(f"check_interval_M={check_interval_M} total return: {total_return}")
        except Exception as e:
            logger.exception(f"Error with M={check_interval_M}: {e}")

    # === Display Best Results ===
    print(f"Best check_interval_M: {best_M}")
    print(f"Best return: {best_return}")


if __name__ == "__main__":
    main()

Ch1. 張鬆允投資心法:

重點:

全面加快賺錢速度,降低賠錢速度與機率。 順勢操作、多空皆宜、資金控管、克服心魔。 兩大自省:進出過於頻繁、買股票一定買最強的龍頭股。 總結:

有部位就有風險,資金控管至關重要。 尊重趨勢,順勢而為。 選股自己下功夫,不靠他人。 進出要靈活,提的起放的下。 當眾人看法一致時,難以賺大錢。 不為交易而交易,盤整時休息觀望。 從經驗中學習,不重複犯錯。 危機可能是價值的真正所在。 Ch2. 張鬆允教戰守則:

多空操作原則:

決斷多空,跟隨市場趨勢。 選股首重流動性。 不見魚兒不撒網,等待指標股啟動攻擊。 面對多頭的心態,股票回檔是休息,不是結束。 試單尋手感,建立部位後觀察趨勢。 總結:

善用指數期貨與現貨同方向操作。 瞭解資券相底餘額,預測市場動向。 運用選擇權成新寵,採用不對稱部位操作。 資金規劃採 4:4:2 配置,應對多頭、空投和盤整行情。 小心海外存託憑證、可轉債、現增套利等虛空單。 Ch3. 千變萬化的期權:

主要觀點:

指數期貨與現貨同方向操作。 未平倉合約判斷多空趨勢。 選擇權成新寵,善用財務槓桿效果。 融資與融券的運用,注意資券相底餘額。 融券放空套利千變萬化。 軋空之後必殺多。 Ch4. 做空獲利寶典:

核心原則:

在多頭與空頭行情都追求獲利。 多空分野一線間,注意行情量與利多不漲的現象。 融資與融券的運用,觀察資券相底餘額。 融券放空套利千變萬化。 軋空之後必殺多。 Ch5. 情緒管理與資金控管:

重要觀念:

抽離市場多數人情緒,保持客觀。 克服貪婪與恐懼。 不隨羊群行動,特別在人多的地方。 利空測試底部,利多測試頭部。 以沒有部位的心態應對市場。 每日檢視投資組合,有明天的觀念。 面對震盪,最好的方法是退場觀望。 資金控管重於一切。 Ch6. 活用基本分析與技術分析:

重要觀念:

先看懂財務報表,重點關注資產負債表、損益表、現金流量表。 股東權益變動表關注股價淨值比、股東權益報酬率等。 重點瞭解現金流量表,掌握公司動態狀況。 股東權益報酬率的計算方法。 資券相底餘額的觀察,預測市場動向。

vectorbt-案例學習(4)-配對(套利)交易

Portfolio.from_orders構造方法

一、獲取資料

import numpy as np
import pandas as pd
import datetime
import collections
import math
import pytz
import scipy.stats as st

SYMBOL1 = 'PEP'
SYMBOL2 = 'KO'
FROMDATE = datetime.datetime(2017, 1, 1, tzinfo=pytz.utc)
TODATE = datetime.datetime(2019, 1, 1, tzinfo=pytz.utc)
PERIOD = 100
CASH = 100000
COMMPERC = 0.005  # 0.5%
ORDER_PCT1 = 0.1
ORDER_PCT2 = 0.1
UPPER = st.norm.ppf(1 - 0.05 / 2)
LOWER = -st.norm.ppf(1 - 0.05 / 2)
MODE = 'OLS'  # OLS, log_return

import vectorbt as vbt

start_date = FROMDATE.replace(tzinfo=pytz.utc)
end_date = TODATE.replace(tzinfo=pytz.utc)
data = vbt.YFData.download([SYMBOL1, SYMBOL2], start=start_date, end=end_date)
data = data.loc[(data.wrapper.index >= start_date) & (data.wrapper.index < end_date)]

二、根據訂單建立投資組合

from numba import njit
# njit為裝飾器,加速運算
@njit
def rolling_logret_zscore_nb(a, b, period):
    """計算a,b(交易對)對數收益率的差值.並進行zscroe標準化"""
    spread = np.full_like(a, np.nan, dtype=np.float_)
    spread[1:] = np.log(a[1:] / a[:-1]) - np.log(b[1:] / b[:-1])
    zscore = np.full_like(a, np.nan, dtype=np.float_)
    for i in range(a.shape[0]):
        from_i = max(0, i + 1 - period)
        to_i = i + 1
        if i < period - 1:
            continue
        spread_mean = np.mean(spread[from_i:to_i])
        spread_std = np.std(spread[from_i:to_i])
        zscore[i] = (spread[i] - spread_mean) / spread_std
    return spread, zscore

@njit
def ols_spread_nb(a, b):
    """最小二乘法計算a,b的回歸殘差(觀測值與OLS回歸線的垂直距離)"""
    a = np.log(a)
    b = np.log(b)
    _b = np.vstack((b, np.ones(len(b)))).T
    slope, intercept = np.dot(np.linalg.inv(np.dot(_b.T, _b)), np.dot(_b.T, a))
    spread = a - (slope * b + intercept)
    return spread[-1]
    
@njit
def rolling_ols_zscore_nb(a, b, period):
    """對回歸殘差的滾動標準化."""
    spread = np.full_like(a, np.nan, dtype=np.float_)
    zscore = np.full_like(a, np.nan, dtype=np.float_)
    for i in range(a.shape[0]):
        from_i = max(0, i + 1 - period)
        to_i = i + 1
        if i < period - 1:
            continue
        spread[i] = ols_spread_nb(a[from_i:to_i], b[from_i:to_i])
        spread_mean = np.mean(spread[from_i:to_i])
        spread_std = np.std(spread[from_i:to_i])
        zscore[i] = (spread[i] - spread_mean) / spread_std
    return spread, zscore

# 滾動OLS回歸分析
if MODE == 'OLS':
    vbt_spread, vbt_zscore = rolling_ols_zscore_nb(
        bt_s1_ohlcv['close'].values, 
        bt_s2_ohlcv['close'].values, 
        PERIOD
    )
#對數收益率差值分析
elif MODE == 'log_return':
    vbt_spread, vbt_zscore = rolling_logret_zscore_nb(
        bt_s1_ohlcv['close'].values, 
        bt_s2_ohlcv['close'].values, 
        PERIOD
    )
else:
    raise ValueError("Unknown mode")
vbt_spread = pd.Series(vbt_spread, index=bt_s1_ohlcv.index, name='spread')
vbt_zscore = pd.Series(vbt_zscore, index=bt_s1_ohlcv.index, name='zscore')



# 生成入場多空訊號
vbt_short_signals = (vbt_zscore > UPPER).rename('short_signals')
vbt_long_signals = (vbt_zscore < LOWER).rename('long_signals')

vbt_short_signals, vbt_long_signals = pd.Series.vbt.signals.clean(
    vbt_short_signals, vbt_long_signals, entry_first=False, broadcast_kwargs=dict(columns_from='keep'))

def plot_spread_and_zscore(spread, zscore):
    fig = vbt.make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.05)
    spread.vbt.plot(add_trace_kwargs=dict(row=1, col=1), fig=fig)
    zscore.vbt.plot(add_trace_kwargs=dict(row=2, col=1), fig=fig)
    vbt_short_signals.vbt.signals.plot_as_exit_markers(zscore, add_trace_kwargs=dict(row=2, col=1), fig=fig)
    vbt_long_signals.vbt.signals.plot_as_entry_markers(zscore, add_trace_kwargs=dict(row=2, col=1), fig=fig)
    fig.update_layout(height=500)
    fig.add_shape(
        type="rect",
        xref='paper',
        yref='y2',
        x0=0,
        y0=UPPER,
        x1=1,
        y1=LOWER,
        fillcolor="gray",
        opacity=0.2,
        layer="below",
        line_width=0,
    )
    return fig
    
plot_spread_and_zscore(vbt_spread, vbt_zscore).show_svg()

img

三、根據交易訊號建構訂單

# 根據交易訊號建立訂單
symbol_cols = pd.Index([SYMBOL1, SYMBOL2], name='symbol')
vbt_order_size = pd.DataFrame(index=bt_s1_ohlcv.index, columns=symbol_cols)
vbt_order_size[SYMBOL1] = np.nan
vbt_order_size[SYMBOL2] = np.nan
vbt_order_size.loc[vbt_short_signals, SYMBOL1] = -ORDER_PCT1
vbt_order_size.loc[vbt_long_signals, SYMBOL1] = ORDER_PCT1
vbt_order_size.loc[vbt_short_signals, SYMBOL2] = ORDER_PCT2
vbt_order_size.loc[vbt_long_signals, SYMBOL2] = -ORDER_PCT2

# 下一個bar執行訂單
vbt_order_size = vbt_order_size.vbt.fshift(1)

print(vbt_order_size[~vbt_order_size.isnull().any(axis=1)])

img

四、進行回測

# 模擬投資組合
vbt_close_price = pd.concat((bt_s1_ohlcv['close'], bt_s2_ohlcv['close']), axis=1, keys=symbol_cols)
vbt_open_price = pd.concat((bt_s1_ohlcv['open'], bt_s2_ohlcv['open']), axis=1, keys=symbol_cols)

def simulate_from_orders():
    """用之前建構的訂單進行回測`."""
    return vbt.Portfolio.from_orders(
        vbt_close_price,  # current close as reference price
        size=vbt_order_size,  
        price=vbt_open_price,  # current open as execution price
        size_type='targetpercent', 
        val_price=vbt_close_price.vbt.fshift(1),  # previous close as group valuation price
        init_cash=CASH,
        fees=COMMPERC,
        cash_sharing=True,  # share capital between assets in the same group
        group_by=True,  # all columns belong to the same group
        call_seq='auto',  # sell before buying
        freq='d'  # index frequency for annualization
    )

vbt_pf = simulate_from_orders()
print(vbt_pf.orders.records_readable)
print(vbt_pf.stats())

img

交易明細

img

績效統計

五、繪圖

from functools import partial

def plot_orders(portfolio, column=None, add_trace_kwargs=None, fig=None):
    portfolio.orders.plot(column=column, add_trace_kwargs=add_trace_kwargs, fig=fig)

vbt_pf.plot(subplots=[
    ('symbol1_orders', dict(
        title=f"Orders ({SYMBOL1})",
        yaxis_title="Price",
        check_is_not_grouped=False,
        plot_func=partial(plot_orders, column=SYMBOL1),
        pass_column=False
    )),
    ('symbol2_orders', dict(
        title=f"Orders ({SYMBOL2})",
        yaxis_title="Price",
        check_is_not_grouped=False,
        plot_func=partial(plot_orders, column=SYMBOL2),
        pass_column=False
    ))
]).show_svg()

img

運行速度:3.72 ms ± 15.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

以上案例為:Portfolio.from_orders構造方法。它非常方便和最佳化的函數用於模擬投資組合,但它需要一些先前的步驟來生成大小陣列。在上面的例子中,需要手動運行散點 Z score的計算,從Z score生成訊號,從訊號建構大小陣列,並確保所有陣列完全對齊。一旦需要測試超過一個超參陣列合,所有這些步驟都必須重複並相應進行調整。

Portfolio.from_order_func構造方法

以下案例為:Portfolio.from_order_func構造方法。 遵循一種不同的(自包含的)方法,其中儘可能多的步驟應該在模擬函數本身中定義。它按順序逐一處理時間戳,並根據使用者定義的邏輯執行訂單,而不是從某些陣列中解析這個邏輯。儘管這使得訂單執行不太透明,因為您不能再即時地分析每一部分資料(Numba 中不能使用 pandas 和繪圖),但它與其他向量化方法相比具有一個重大優勢:事件驅動的訂單處理。這提供了最大的靈活性(可以撰寫任何邏輯)、安全性(降低了暴露自己於前瞻性偏見等其他偏見的可能性)和性能(資料只需要遍歷一次)。這種方法與 backtrader 最為相似

一、主程序

from vectorbt.portfolio import nb as portfolio_nb
from vectorbt.base.reshape_fns import flex_select_auto_nb
from vectorbt.portfolio.enums import SizeType, Direction
from collections import namedtuple

Memory = namedtuple("Memory", ('spread', 'zscore', 'status'))
Params = namedtuple("Params", ('period', 'upper', 'lower', 'order_pct1', 'order_pct2'))

@njit
#該函數為每個組(一對列)準備資料。它初始化陣列以儲存價差、z 分數和狀態。它選擇當前組的參數
#並將它們儲存在一個容器中。
def pre_group_func_nb(c, _period, _upper, _lower, _order_pct1, _order_pct2):
    """Prepare the current group (= pair of columns)."""
    assert c.group_len == 2
    
    # 與bt相比,vbt不建立實例,而是儲存在陣列裡
    # 建立spread和zscore陣列:這些陣列用於儲存價差和z分數。它們被初始化為NaN,長度為c.target_shape[0],
#即資料的長度。
    spread = np.full(c.target_shape[0], np.nan, dtype=np.float_)
    zscore = np.full(c.target_shape[0], np.nan, dtype=np.float_)
    
    # status陣列:這個陣列用於儲存狀態資訊,初始化為0。
    # 將spread、zscore和status陣列組合成一個命名元組,作為儲存資料的容器。
    status = np.full(1, 0, dtype=np.int_)
    memory = Memory(spread, zscore, status)
    
    # 將傳入的參數_period、_upper、_lower、_order_pct1、_order_pct2轉換成陣列,並根據組的索引選擇對應的值。
#這樣可以確保每個組可以有不同的參數組態。
    period = flex_select_auto_nb(np.asarray(_period), 0, c.group, True)
    upper = flex_select_auto_nb(np.asarray(_upper), 0, c.group, True)
    lower = flex_select_auto_nb(np.asarray(_lower), 0, c.group, True)
    order_pct1 = flex_select_auto_nb(np.asarray(_order_pct1), 0, c.group, True)
    order_pct2 = flex_select_auto_nb(np.asarray(_order_pct2), 0, c.group, True)
    
    # 把所有參數放入容器
    params = Params(period, upper, lower, order_pct1, order_pct2)
    
    # 儲存 pre_segment_func_nb 函數中要用到的兩個目標百分比。這個陣列的長度為當前組的長度
    size = np.empty(c.group_len, dtype=np.float_)
    
    return (memory, params, size)
    

@njit
def pre_segment_func_nb(c, memory, params, size, mode):
    """預處理分段資料"""
    
    # 檢查是否達到指定窗口大小
    if c.i < params.period - 1:
        size[0] = np.nan  # size of nan means no order
        size[1] = np.nan
        return (size,)
    
    #窗口切片:用於計算zscore
    window_slice = slice(max(0, c.i + 1 - params.period), c.i + 1)
    
    # 根據不同模式計算價差spread
    if mode == 'OLS':
        a = c.close[window_slice, c.from_col]
        b = c.close[window_slice, c.from_col + 1]
        memory.spread[c.i] = ols_spread_nb(a, b)
    elif mode == 'log_return':
        logret_a = np.log(c.close[c.i, c.from_col] / c.close[c.i - 1, c.from_col])
        logret_b = np.log(c.close[c.i, c.from_col + 1] / c.close[c.i - 1, c.from_col + 1])
        memory.spread[c.i] = logret_a - logret_b
    else:
        raise ValueError("Unknown mode")
    spread_mean = np.mean(memory.spread[window_slice])
    spread_std = np.std(memory.spread[window_slice])
    memory.zscore[c.i] = (memory.spread[c.i] - spread_mean) / spread_std
    
    # 根據 z-score 是否超過設定的上下界,確定要執行的交易操作。
#如果 z-score 超過了上界,則賣出第一列資產並買入第二列資產;
#如果 z-score 低於下界,則買入第一列資產並賣出第二列資產。
    if memory.zscore[c.i - 1] > params.upper and memory.status[0] != 1:
        size[0] = -params.order_pct1
        size[1] = params.order_pct2
        

        c.call_seq_now[0] = 0
        c.call_seq_now[1] = 1
        memory.status[0] = 1
    elif memory.zscore[c.i - 1] < params.lower and memory.status[0] != 2:
        size[0] = params.order_pct1
        size[1] = -params.order_pct2
        c.call_seq_now[0] = 1  # execute the second order first to release funds early
        c.call_seq_now[1] = 0
        memory.status[0] = 2
    else:
        size[0] = np.nan
        size[1] = np.nan
        
    # 根據執行順序 call_seq_now,決定先執行哪一筆交易。這裡指定了先執行賣出操作,再執行買入操作,以及先執行買入操作,再執行賣出操作的情況
    c.last_val_price[c.from_col] = c.close[c.i - 1, c.from_col]
    c.last_val_price[c.from_col + 1] = c.close[c.i - 1, c.from_col + 1]
        
    return (size,)

@njit
def order_func_nb(c, size, price, commperc):
    """生成訂單."""
    

    group_col = c.col - c.from_col
    return portfolio_nb.order_nb(
        size=size[group_col], 
        price=price[c.i, c.col],
        size_type=SizeType.TargetPercent,
        fees=commperc
    )
#主程序,用Portfolio.from_order_func回測策略
def simulate_from_order_func():
    """用Portfolio.from_order_func回測策略`."""
    return vbt.Portfolio.from_order_func(
        vbt_close_price,
        order_func_nb, 
        vbt_open_price.values, COMMPERC,  # *args for order_func_nb
        pre_group_func_nb=pre_group_func_nb, 
        pre_group_args=(PERIOD, UPPER, LOWER, ORDER_PCT1, ORDER_PCT2),
        pre_segment_func_nb=pre_segment_func_nb, 
        pre_segment_args=(MODE,),
        fill_pos_record=False,  # a bit faster
        init_cash=CASH,
        cash_sharing=True, 
        group_by=True,
        freq='d'
    )

vbt_pf2 = simulate_from_order_func()
%timeit simulate_from_order_func()

img

運行速度:4.4 ms ± 17.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

二、最佳化速度-加入numba:

def simulate_nb_from_order_func():
    """用`portfolio_nb`回測."""
    # iterate over 502 rows and 2 columns, each element is a potential order
    target_shape = vbt_close_price.shape
    
    # number of columns in the group - exactly two
    group_lens = np.array([2])
    
    # build default call sequence (orders are executed from the left to the right column)
    call_seq = portfolio_nb.build_call_seq(target_shape, group_lens)
    
    # initial cash per group
    init_cash = np.array([CASH], dtype=np.float_)
    
    order_records, log_records = portfolio_nb.simulate_nb(
        target_shape=target_shape, 
        group_lens=group_lens,
        init_cash=init_cash,
        cash_sharing=True,
        call_seq=call_seq,  
        segment_mask=np.full(target_shape, True),  # used for disabling some segments
        pre_group_func_nb=pre_group_func_nb, 
        pre_group_args=(PERIOD, UPPER, LOWER, ORDER_PCT1, ORDER_PCT2),
        pre_segment_func_nb=pre_segment_func_nb, 
        pre_segment_args=(MODE,),
        order_func_nb=order_func_nb, 
        order_args=(vbt_open_price.values, COMMPERC),
        close=vbt_close_price.values,  # used for target percentage, but we override the valuation price
        fill_pos_record=False
    )
    
    return target_shape, group_lens, call_seq, init_cash, order_records, log_records

target_shape, group_lens, call_seq, init_cash, order_records, log_records = simulate_nb_from_order_func()
print(vbt.Orders(vbt_close_price.vbt.wrapper, order_records, vbt_close_price).records_readable)

img

vbt_pf3 = vbt.Portfolio(
    wrapper=vbt_close_price.vbt(freq='d', group_by=True).wrapper, 
    close=vbt_close_price, 
    order_records=order_records, 
    log_records=log_records, 
    init_cash=init_cash,
    cash_sharing=True, 
    call_seq=call_seq
)

print(vbt_pf3.stats())

img

列印績效統計

運行速度:2.3 ms ± 9.23 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

交易的行為與思維分享

https://disp.cc/b/DigiCurrency/eIYg

各位好久不見,這次是用約10萬經過五天左右變為約1000萬

標的物是Luna幣(也不用問我這是什麼,因為我也不知道她能幹嘛)

我只做技術派的價格區間獲取,跟隨趨勢來賺錢

這次比起之前獲利金額算是較小(前面的文章30萬變3000萬),但其實倍率是差不多約單 筆翻了100倍

先上對帳單圖分享這次做單的技術,後續再提供來信的問題回覆與交易思維

[圖]

[圖]

[圖]

[圖]

[圖]

[圖]

有幾個重點可以提供參考

1.仍然都是一貫的手法,原則以抓到大波段的起漲點後,透過未實現獲利持續加碼

將成本與強平價格墊上去,最終強平價格高於開倉,代表即使被強平仍然是賺錢的。

2.持續強調我的交易策略:做標的只做強勢標的。

市場資金不會完全瞬間歸零,會有其動態平衡,聰明錢會自己往強勢標的集中,

跟著強者走會比跟著弱者取暖有用(不只交易,人生也是這樣),

不只上一次讓我大賺的SOL到這次的LUNA都是同樣道理。

3.不是每個人都能經歷獲利回吐處之泰然的階段,有跟單的、有看戲的,

在我未實現獲利從+550萬回吐到+250萬時,絕大部分的人都是受不了而趕緊出清,

但,我原本的風控機制已控制在可以接受的範圍(最差就是+100萬收場),

為何還要隨之起舞?

4.為了達成心理目標(10萬變1000萬),所以在最後關頭用獲利再度加碼BTC

(其實是交易所限制我再加碼原標的LUNA了,不然這張單破2000萬沒問題)。

原因是如果是萬物齊漲,那我至少要抓到平均以上的漲幅,

所以就選中規中矩的BTC來做,屬於將上述第一點更進化。

以下分享持續收到的問題回覆,提供各位參考

當然,還是要講那句老話,能不能賺錢靠的是心態,絕非技術

「成功的交易(員),80%靠心態,技術僅佔20%,甚至不到」

1.做短做長哪個好?

其實要看自己個性,能賺錢的當然都是好方法,

管你用無限攤平大法還是高頻tick交易,

不要看人短線做得厲害就想做短,看人長線賺大錢就決定要做長線,

找出自己適合吃的餡,之後根據此策略再去進化,

而不是一直在做反覆檢討系統的可行性報酬率,該檢討的是「自我適性的策略」。

但我直接告訴各位,一般普通人要達到財富累積的手段絕對是依靠趨勢的大波段來積累,

絕對不是靠著短進短出達成,即使有,那都是非常短暫且非常少數,

如果您不相信,您可以持續短沖當沖個幾年試試,絕大部分都是賠了資產又賠了身心。

(我以前也做過短沖,那是非常痛苦操作,分k越看越小,搞得連吃飯睡覺都無法安心)

另外也可以去思考邏輯,如果您連操作一兩次的大波段都沒辦法吃到豐厚獲利,

又怎麼期待自己能夠在短線快速決策中持續做對方向,

很多人很愛抓轉折,越抓越容易骨折、財富打對折。

2.看盤要看什麼?

許多人常掛在嘴邊說要專心看盤,

但其實,如果做的是趨勢,並不需要因為盤中的些微震盪走勢而緊張兮兮,

要跟上的是海裡潮流,而不是表面波浪,所以話說回來,天天看盤有必要嗎?

看了看出些什麼東西?還是隻是安慰自己有「認真做起看盤」這件事,

絕大部分都是在「祈禱」與「取暖」而已,不是嗎?

交易就是決定下手後,是輸是大贏交給上天運氣去決定,

能做的只有控制住虧損、控制住情緒,不要太主動介入原先的交易,

這樣檢討起來才有意義,做交易到最後其實只是期望值的博弈而已,

如我這筆單最後的進出階段也不是技術分析了,

而是我能忍受最差就只賺100萬,但我有機會拚1000萬時的取捨罷了。

再說了,你的金額可以大到影響趨勢嗎?

如果不能,那你到底在看什麼盤讓自己緊張兮兮,

反正都是在賭博,倒不如去賭場還能享受美食、美女、美好氛圍吧。

3.為什麼不做空?

長期追蹤IG的可以發現我很少做空,即使有也都是非常短暫猜空,且金額會縮得非常小。

做空是極度危險的,看看近期的瘋狂鎳與石油,縱然您天生神力賺了一輩子,

但是遇到系統性風險、流動性風險時,嘎空是無上限的,

做空可以把您的資產在一夕之間全部化為烏有,做多了不起就歸零損失100%。

試想想,既然普遍都說做多賺得多,做空賺得快,若要非選一個,還是選「賺得多」吧?

所以前陣子常有私訊問候「做多賠錢爽不爽」、「北七,熊市幹嘛不做空」,

我都笑笑回應,如果空頭我都能尻百萬千萬了,還會怕熊市?

況且,還沒看到做空爆賺的單子,為什麼?因為空頭走勢不會一次到底,

加空、追空遇到盤中嘎空被逼得吐出倉位與回補,整體效益其實非常的低。

4.為何選擇做波段卻常常遇停損?

做波段者目標是享受豐厚的獲利,但在此之前要認清一件事,

絕大部分的走勢都有七八成時間在盤整,

也就是說並不可能有人或有標的可以一直做到大波段,有的話就是詐騙。

如同我這筆單全壘打之前,也是不停的停損

(持續約虧損200多萬,這都有在IG提到,我也會賠錢),

遇到盤整的撞牆期靠的是資金控管與避免心態崩壞,

首先要有良好的的資金控管才能避免心態崩壞,

可透過縮小倉位、資金部位、槓桿、下單頻率等方式來達成目標,

在逆風的時候要先保護自己的城池,否則順風來臨時,你已無銀彈可以逆轉勝。

5.盤感如何培養?

「感覺是最沒根據的策略」

無論你是用基本面EPS、技術面K棒、籌碼面數據做為交易系統,

至少這些都是客觀的存在事實,再經由交易系統消化後執行進出決策。

但很多人都靠「感覺」在交易,要戰爭了感覺要跌了,要和談了感覺要噴了,

甚至還會感覺主力莊家大戶該怎麼做,

主力莊家大戶們都在看電影、逛街、吃美食、做SPA、陪家人朋友玩樂,

並不會去在意1分、5分K等極小的跳動好嗎?

感覺會因為當下的氛圍而受影響,同一件事在不同環境下的感覺又會不一樣,

做交易最忌諱用感覺做,「我都感覺到你感覺快輸光的感覺了」。

其實還是講得很亂我知道,想到甚麼講什麼,

交易沒那麼困難:化繁為簡,持盈保泰,穩定累積,財富自來。

最後澄清幾點

1.本人僅有PTT帳號與小編經營IG,並無其他社團TG等,更無與任何交易所合作,

亦無發行NFT,請勿被來源不明的連結受騙。

2.實在不用連續虧損時來私訊騷擾,獲利豐厚時來討好,

就算這都是運氣好了,連續好運氣也是一種實力了吧(?)

3.先前說要發奶茶,避免造成疫情的關係改為持續捐款,希望社會能更好

偏好捐款給不能控制出生環境的弱勢孩童們

因為孤苦老人的背後,並不知道是不是年輕時胡亂凹單造成的

投資(投機)的觀念與心得分享

本來想用「30萬3天賺3000萬」當標題

但想想可能過於聳動會害到正確的投資觀念,故作罷。

持續收到很多來信詢問,就打一篇分享給各位參考

投資邏輯策略甚至是心態養成也絕對不可能僅靠一篇文即可達成,需要持續修練

先給幾張鄉民最愛的截圖(九月份統計與曾經的未實現損益)

[圖]

[圖]

[圖]

[圖]

[圖]

虛幣近期的操作運氣不錯,常常運氣不錯的話,就需要點實力

整個九月份總計約+90萬鎂(+130萬鎂與-40萬鎂),初始本金為1萬鎂

勝率與盈虧比在月底時因盤整開始下修,尤其盈虧比由4.5下修至2.5左右

嚴格算應該是月初時3天內+100萬鎂,接下來都在損益兩平打醬油居多

前情提要

1.從大學時期就對研究投資領域深感興趣,大學時期就從股票開始做起

後來因緣際會下轉戰國外外匯期貨原物料,直到最近半年較開始積極操作虛幣

所以整體投資(投機)經驗歷程有相當的時間,

累積下來的進出操作次數可能比吃過的飯還多,至少,目前還存活在市場上: )

2.本人完全技術分析派(曾經也使用過各種派別)

堅信在任何基本面、消息面、應用面、籌碼面等各類陽春麵、牛肉麵

最終都會經過市場消化後反映在價格走勢上。

所以坦白講,對近期操作過的幾支貨幣則完全不瞭解在幹嘛用的

對我來說只要走勢型態符合我想要揮棒的好球,就會揮棒,化繁為簡,簡單穩定。

以下幾點投資(投機)觀念分享給各位

「成功的交易(員),80%靠心態,技術僅佔20%,甚至不到」

「成功的交易(員),80%靠心態,技術僅佔20%,甚至不到」

「成功的交易(員),80%靠心態,技術僅佔20%,甚至不到」

很重要,所以說三次,這第一項的觀念最重要

沒有時常把這句放心中,以下再多的正確觀念、再強的技術都沒用

如果沒有穩定的心態素質與遇挫敗冷靜處理的情緒,

即使有超過95%比神還神的神奇指標工具,最終也會因為那不到5%的風險出現一夕翻船

2.「能抓老鼠的就是好貓」

上述有提到我是以技術分析來操作,但我不會鄙視任何的方法

只要能夠有一套穩定的交易策略邏輯,哪怕是參考農民曆投資術只要能賺錢就是好方法

因為我們來到市場的目的是要為了獲取財富改善生活,而不是做論文爭辯你輸我贏

只有贏過市場才是唯一目標,既然目標已有,

那應該把重點放在體認自我個性何種方式適合自己

明明不適合作短線的料,看著旁人利用策略賺快錢,盲目跟從,烙賽居多。

明明個性耐不住抱好幾年,卻羨慕N年前比特幣買披薩的價位,

且說白了,即使買了也不可能抱到現在

別羨慕有人能「睏霸數錢」,因為你如果下完單無法安心去睡,整天緊張兮兮

最重要的睏霸都做不到,是要如何睡醒數錢

唯有先了解自己,才有辦法克服自己,找到舒服的獲利方式。

3.「現貨、槓桿、合約保證金」

投資投機的順序千萬不能錯,先做現貨,

成功了再試著提槓桿,駕馭了,再嘗試合約保證金

大部分輸慘的行為都是順序相反,先做合約,

輸光了只好被動降槓桿,再輸沒錢只能買現貨

如果還不會騎腳車,那就千萬別學騎機車開汽車,穩定了再往開高鐵開飛機的交通工具

相反的,如果順序顛倒,遇上狀況無法處理那就是整車對撞

切記,如果現貨都無法賺錢,走上槓桿保證金只是死路一條

4.「試著控制虧損,不要想控制獲利」

市場上千變萬化,走勢千百種,能做到的只有控制虧損,期待獲利

既然能賺錢就代表走在趨勢道路上,不要每次下單後遇見獲利就急欲分手,專情一點

每當我下單時,最大的虧損就在下單那刻已決定訂死,

只要走勢如預期就是不停上調停損直至出場條件達成時,

虧損只會越來越小甚至即使出場仍然是獲利

當能夠駕馭虧損風險時,就能再繼續雕琢進出場點來增加報酬

5.「勝率不是唯一,豐厚獲利才是」

效率市場對於決定下單後的勝敗是快速展現,

要能夠一直承認自己犯錯而停損是非常違反人性的

但也因為人性不喜歡認錯,所以才會凹單,10次凹對進而凹下一次,一次凹錯就抬去種

大部分績效有問題的都是數次小賺抵不過一次大賠,

想辦法讓自己的績效成為多頭排列格局

穩定不落差太大的勝率,加上適當風報比例的搭配(通常我都抓2.5至3以上)

最後結果就是大漲小回(大賺小賠)

6.「本多不一定終勝,本少也不一定賺不了大錢」

這邊要特別澄清,因前陣子對帳單被分享出去,

有人誤以為是因為本金大才有大數字獲利

但其實本多終勝,這句話是對也是錯,希望別被誤導了

如果今天做的是現貨,那本多的確絕大部分都是對的,

就算當下錯了,可以靠著時間來彌補錯誤

(凹一輩子凹不過,也可以隔代凹XD)

如果今天做的商品有牽扯到槓桿與時間性,沒有正確的觀念即使再多的本,

縱然有幾千萬幾億的本金也是會財富轉移全軍覆沒

另外,本金大是否有辦法扛的起金錢數字跳動,

每秒幾十萬(有些人是百萬千萬)的跳動還能冷靜處理,這又是另一個心態層級的問題了

至於本小則有本小的彈性,操作策略靈活與可用工具較多,

並非無法由小錢變大,即使要這本金腰斬也要經過連續性的挫敗與情緒崩壞

但在良好的風控下,理論上是可以穩定向上,遇上良好契機一次打出滿貫砲

7.「永遠不要失去希望」

如果現在放棄,比賽就輸了。

厲害的不是一蹴成功,而是跌倒了能夠再次站起來,那才是下一次風險趨避的內化。

成功的交易員絕大部分都會經歷過挫敗、破產甚至負債的狀況,

也唯有經歷過才知道在市場上永遠要謙遜且將風險擺在第一

最後也捐出部分獲利給弱勢團體(就不貼在這了),

持續在群內提倡的就是若有賺錢且能力可及之下

將部份獲利幫助弱勢團體(尤其是孩童),形成正向循環,

讓投資成就體現在生活上,而非只是紙上富貴而已。

其實,要講可以講很久,但也很難一次講完就都貫通,

只能把臨時想到的列出來給大家參考

期待各位績效穩定向上,加油。週末愉快

等疫情允許後,會再請人發送奶茶,謝謝

投資(投機)的觀念與心得分享(2)

雖然上一篇說過了心態第一,技術其次,但仍然有不少來信問到技術分析

我可以分享我的方式,因為就連跟單都會跟丟了,我也不怕分享我的方式

最終即使學會所有招式,沒有相對應的心態配合

一樣都是跟出事的比跟出師的還多

其實我覺得投資行為(投資方式)跟棒球比賽很像

後續會試著比喻解釋給各位去理解

可能您會難以認同或接受,但這是目前我在市場生存的方式

我也不會去鄙視長期持有派的方式,因為我自認無法達成那樣的耐性

我希望在我手上的資金在目前人生階段是高效的應用

當然,我就得承擔高效背後的成本與虧損(時間成本、交易摩擦成本、行為成本)

而且,很常看到有人說航海王、期貨王這樣天天賺不就很快超越巴菲特了

其實,如果享受過高本金帶來的效率與虧損速度

就能理解賺到的錢只是做其他資產配置而已

大部分有此階段的都不會持續一股腦投入,會將部分資產轉往低風險配置

萬一,哪怕只是0.00000001%機率出事,那不就人生完蛋?

來市場的目的是過更好的生活,不是誰要超越誰,不是嗎?

廢話不多說,以下就我使用的技術判斷方式做分享

1.「指標大部分都是落後指標」

我們通常會喜歡去設計工具、指標、參數最佳化等等的方式來預測價格變化

但,請各位想想,手上的神奇指標工具是否絕大部分都是從

「價格」、「時間」、「動能(量能)」等原始原料變化計算而來的

那麼,我們卻要利用價格計算後的結果去預測未來價格

對我來說,那已經是過去式了,最貼近價格變化的就是「價格」

價格體現在技術分析圖譜上就是「K棒」

一根「K棒」可以看出市場當下的買賣力道的行為變化

一群「K棒」可以看出市場趨勢下的買賣力道的行為變化

因此我的看盤工具就單純只是一支IPHONE+三條均線

沒有太複雜的介面工具,沒有華麗的指標進出,單純就K棒與均線

(這也是常被群裡靠腰的原因,不專心學,重點都放在截圖裡電池電量偏低XD)

如果此時你好奇我均線的參數,那又回到此題初始命題

“過度的追求最佳化參數化的行為”

況且,均線也就只是價格的平均做呈現,仍然是屬於落後指標

「均線對我而言是判斷趨勢,不是用來判斷點位」

所以才有很多說什麼黃金交叉、死亡交叉不準啦騙人啦

2.「強者恆強,跟人一樣」

一群標的裡面,以任何的平均概念,至少能分出強勢與弱勢各一半

再從各一半的群體裡面去找尋想做單的標的

以臺股而言我就抓大盤加權指數,大盤是一個群體的概念

比她弱的股票代表輸給一半的股票績效

加密貨幣我則以比特幣為基準,做多我選比她強勢,做空我找比她弱勢

這樣至少確保我能贏另外一半的績效

這其實跟人很類似,跟人性也類似

人的群體裡就是強者恆強,尤其在交易市場的行為呈現更是如此

與其期待輸家翻身,不如壓注贏家的概率報酬

你是總教練的話,你會排強棒還是排弱棒來贏得比賽

當然是把隊上的強打者排上打線,拚獲勝機率

3.「追價行為大可不必」

不是不能追價,而是不知如何使用時卻誤用、混用,通常下場都是虧損居多

因為追價行為就是一種過度曝險,長期統計要賺錢的必然結果是盈虧比要好

盈虧比在追價行為下要維持穩定

要馬就是追價後的價格順利突破比原先預設的出場點還高

不然就是要縮小停損點區間來控制虧損比例

不管哪個方式對於操作來講都不容易達成

而且,試問,為何要做出這種行為?因為通常都是情緒心態使然

怕現在不買買不到了、怕沒機會了、怕再不放空就要跌死了

「情緒告訴你快要失去財富自由機會了,但通常都是失去財富」

如果沒等到好球,就盡量別去追打球、追打壞球

世上沒有哪一個強打者是以追打球而成功的,都是鞏固好自己好球帶

即使沒球打選到保送(等待下次機會),也不會出局。

該做的是「追逐趨勢,而非走勢」。

4.「你的空頭是我的多頭」

使用的時間週期不一樣,預判的想法自然會不一樣

試著讓自己的進出條件一致,出錯也比較方便事後檢討

單純針對自己的標的物做判斷

常被問到會不會參考大哥走勢,怕突然插針之類的

奇怪捏,會怕的話幹嘛不做大哥就好,這樣要不要擔心明天天氣影響的蝴蝶效應

擔心太多擔心不完,擔心自己手上的走勢比較簡單

也常被問如何看,怎麼看,我通常都回用手機IPHONE看阿

別人的看法只是讓自己心安,讓自己鴕鳥

你看出的空頭,可能只是我週期裡的多頭修正支撐,做出相對應的動作即可

下次再問怎麼看,不如外出走走看看河岸、看看夕陽很美。

5.「要如何調整停損、停利」

這也是一直被問到的,常常走勢如預期方向走,但調了又回來把部位掃掉

最基本的原因就是太快調整,太快調整的背後原因就是怕虧損或怕獲利跑掉

這就又回到一開始講過的心態問題,怕虧損就降低槓桿、降低下單倉位

會怕獲利跑掉只是因為沒有嚐過一次大賺的甜頭

享受過豐厚報酬自然不會怕,反而會擔心正在獲利的時候倉位不見

而技術上,要先認清你自己的週期

你策略是60分K週期進場,然後因為15分拉出長紅長黑後就調整

下一根出現反向吞噬就直接把你掃掉,然後60分K仍然照著既定的時間續走

最後60分K走出大波段,才在哀怨是莊家掃我啦,故意的啦

是自己害自己的吧?關莊傢什麼事?

莊家會動用一堆資金為了掃你小單嗎?會不會覺得自己太重要了?

再來60分K的價格波動如果平均是1%,你在此週期進單下設0.3%停損

請問是在送錢還是錢太多在玩大富翁,哈囉哈囉!沒有紅卡黑卡就醒醒吧!

其餘有想到再編輯補充好了,先來去吃個豐盛早餐(中年肚子越來越大惹)

投資(投機)的觀念與心得分享(3)

開始前先提一下近況,自從9月多海尻三千萬後

直到最近都沒有太大的績效起伏,甚至總體來說賠錢居多

這是因為順勢交易遇到盤整必然會開始呈現撞牆期

撞牆期要克服的就是如何降低虧損,詳細後續會說明

(不管何時何種的交易方式,都是以降低風險為第一考量)

近期較成功的一筆單子則是LUNA

初使本金20萬,最高未實現獲利約400萬,實際獲利約180萬,可惜沒一鼓作氣衝上去,不 然也是筆千萬單了,運氣沒有來

總計耗時3天(幣圈算30年?),操作方式仍然是一貫的拿未實現獲利做加碼,也因此餘額 本金維持在20左右,後續也是同樣的被系統風控降槓桿在5-10倍

[圖]

[圖]

[圖]

[圖]

(感謝有心人幫忙整理此圖)

分享幾個技術方法與心法給各位

當然還是要強調原文的第一點

「成功的交易(員),80%靠心態,技術僅佔20%,甚至不到」

\1. 「上漲下跌的型態都差不多」

會上漲會下跌的型態都長得差不多那樣,那是因為趨勢才是真正的力量 順著趨勢會有 一定的脈絡可循

從最小的點位,產生了價格K棒,再由數根K棒形成面向

最後由這些不同呈現方式形成了型態

也因此常常有人問我要怎麼增進功力

忘了有無提過,剛開始接觸時曾經每天將所有臺股的線圖都看過一遍

這個功課至少做了一年,真的就是每天看每天熟悉

現在如果有空我也會偶爾看看,大致能知道臺股現在的狀況及找到好標的

當時也許全部看過需要花個三小時以上,現在則是三十分鐘內搞定

漸漸的自然而然會有一種感覺,如同一開始講的

「會上漲會下跌的型態都差不多,瞄一眼就知道會漲會跌哪個機率大」

成為真正的大廚師前,哪位不是在廚房先洗碗洗韭菜

學會少林功夫前,哪位不是先掃地打坐好幾年

別小看這種持續累積的力量,即使告訴你這麼做可以增進功力

還是有人無法做到,其實這也是練習心性的一種方式,穩定性

2.「下跌找鑽石、上漲防爛貨」

從K棒型態可以知道當下發生什麼事,也因此從下跌走勢中更能看出抗跌標的

當時的SOL、前一陣子的SAND、及最近的LUNA都是這樣找到的 (這裡要偷偷抱怨SAND,當時掛了4.82要接貨,結果最低只到4.84後一路噴向8,此筆單如 果能夠吃到也是海撈千萬的單子了,人森啊QQ)

至於為什麼會抗跌我也不會去深究,我就是知道當下她特別抗跌

有人買就不容易跌,這些人這些力量為何敢在下跌崩跌閃崩時去買

我不需要知道理由,我只需要跟著這些趨勢動能力量即可

願意在下跌中去進場吃貨的標的,其未來上漲的機率絕對遠大於其他標的

相反的,在市場多頭同步上漲時,絕對有落後的標的

此時反而要特別留意手上的標的是不是特別弱勢

防止自己因為盤勢大好的時候而忽略了績效落後的風險

至於怎麼找怎麼判定則是更進階的技術,後續有機會再討論

3.「順勢交易的撞牆期」

趨勢一旦成型,勢如破竹擋都擋不住

這是因為一旦走勢方向呈現更種訊號買盤都一致認同的時候

會是正向的動能持續累加上去,此時是最容易賺錢的時刻

但往往絕大部份的人都會在此時迷失在賺錢的快感中

客觀來分析,走勢不可能永遠多頭永遠空頭

漲勢中必然會有「盤整修正」,跌勢中必然會有「盤整反彈」

也因此絕對會有出現「盤整」的時刻

遇到盤整時刻可以從自我績效去發現

如果都固定使用順勢交易,但績效開始呈現不穩定

那很容易察覺是順勢策略不適合現在的盤整走勢

必然會遇到撞牆期的震盪與獲利回吐

該如何克服有幾種方式

A完全不作單,這方式最容易避免虧損,但我知道,這點也是最難做到的,要一個賭客完 全不下場簡直比登天還難

B倉位逐漸縮小,假設策略可行且策略不變,遇到連續虧損時代表盤整機率大,適時地縮 小倉位可以保護你長存在市場中,也就是俗稱的「輸要縮」

C每一次大賺之後再將其資金拆分,等分的去做而不是無上限利滾利,通常都會死在最後 一次。但如果有做拆分慢慢玩,賺一次夠我隨心所欲玩個十次二十次連續停損都沒關係, 只要這幾十次中再被我遇到大行情就夠了

4.「情緒穩定有助於交易」

前幾天有人在IG私訊說該不該FOMO追價LUNA

如之前提到,追價的行為已經將風險無形中擴大

而且這擴大影響不是隻有短期該筆單

如此的不理性長期行為模式,會導致你走向毀滅的局面

通常遇到這種狀況的時候,因為大腦已經不理智了 我會告訴自己現在做多好還是做空 好,重複兩三遍讓自己稍微回到理性

再來可以先切畫面播放首歌聽完再做決定,往往可以避開很多大賠的狀況

冷靜期是要練到自己剋制自己,而不是靠系統幫你剋制下單

5.「主動打破紀律與被動維持紀律」

當你可以做到情緒穩定時,當然可以主動打破紀律

主動打破紀律目的在於盡可能降低虧損(即使停損點位還沒到)

這是因為綜合經驗與能力之後所決定,認為此時以較少虧損換取未來大虧損的機率

當然,你就勢必要承擔主動介入後的錯誤決定

這點並不是要你每次都盯盤看盤,然後妄猜想去主動做決定

而是要能夠融會貫通之後,即使自己主動介入後結果不理想,仍然能處之泰然

然後將這個情緒化壓到最低的位階不再影響下一筆作單

很多人也會在此時遇到「見山不是山」的局面,開始懷疑人生

那麼就乾脆回到原點重新再練

被動的維持紀律,該筆單該筆標的只會錯一次

主動介入則會有一錯再錯的無限可能

以上是我近期收到一些來信與IG私訊的回覆,盡可能把想到的回覆,但很多人都喜歡問「 加碼」這件事,沒先把基本功學好就學加碼,很容易萬劫不復,有機會再分享了

其餘心得等下次有機會再尻大筆單的時候再來獻醜吧,祝大家TO THE MOON~


1.順勢而為
2.用基礎的均線進場
3.趨勢是長時間價格變化的過程,不會只有一波,後面會有好幾波,不會太在乎第一波,很難看對第一波發動,與其猜第一波,趨勢形成進場就可以了,後面吃23456789波,吃魚肉,魚頭魚尾留給別人
4.用自己的週期去判斷走勢,抓單一週期操作比較不會失真(4h/日/週/月…)
5.心態的波動,與金額大小有關,每個人都有自己的承受度
6.進場有停損點,最多就賠這些錢,進場前已經先評估完成
7.不可能每一次都賣在最高點,達到設定目標的單,就可以開始停利跟移動調整停損價格,都是獲利,回測太深沒關係,畢竟停損已經往上修正
8.情緒跟市場沒有正相關,不用太在意市場雜訊
9.常停損原因,固定”行為”或是固定”策略”出了問題?需復盤,檢討績效,是行為問題?還是策略問題?
10.操作連敗也沒關係,修正之後,以後應該會減少,連敗可能也常發生,舉例上半年,沒有波動,很常上半年都在停損,上半年可能就修正停損點,心態要穩定,行為+策略需檢討,連敗2~3次,很常發生,若10次以上需復盤檢討
11.沒有用太多工具,基本上就交易所的K線圖,工具/指標的背後意義要先了解,才能針對市場操作,像是做趨勢,就以30日/45日/60日均線
12.圖表會有相似度,看久了會有共感,久了容易找到標的,也可從強勢標的找,如果漲勢跟趨勢符合就從裡面挑選
13.不太看消息,所有的訊息都會呈現在價格上,不好復盤
14.進出場點位否符合進出場邏輯,如果檢討之後發現有點偏離自己的策略或是行為,這次的單,心態不能太膨脹,錯過的單也可以檢討,自己的策略在這個圖形是否能吃到,沒吃到是否需檢討?若沒有,維持一樣既有模式就可以了,在自己的守備範圍內賺到該賺到的錢就可以了
15.專注多方原因:上漲無限,下跌有限
16.對於幣圈整體大環境,ETF如果走入大眾生活,應會是正向的發展


「趨勢成形, 創新高,沒出量 沒爆量→穩穩漲定了」

基本面:不會看
籌碼面:不知道
消息面:不相信
技術面:
1、三線走揚,多頭排列,中期確立
2、近期紅K盤整抵抗,支撐穩定
3、未見爆量,量價齊揚

4. 進退場機制:
進場價:28以下
停損價:24
目標價:40
風報比:約3可以接受

低波動 飆股長相

def compute_candle_volatility(timeperiod=20):
    close = data.get("price:收盤價")
    high = data.get("price:最高價")
    low = data.get("price:最低價")
    open_ = data.get("price:開盤價")

    bullish_candle = close >= open_
    bullish_volatility = (
        abs(close.shift() - open_)
        + abs(open_ - low)
        + abs(low - high)
        + abs(high - close)
    )
    bearish_volatility = (
        abs(close.shift() - open_)
        + abs(open_ - high)
        + abs(high - low)
        + abs(low - close)
    )
    candle_volatility = FinlabDataFrame(
        np.nan, index=close.index, columns=close.columns
    )
    candle_volatility[bullish_candle] = bullish_volatility
    candle_volatility[~bullish_candle] = bearish_volatility
    volatility = (
        candle_volatility.average(timeperiod) / close.average(timeperiod) * 100
    )
    return volatility

收盤價跟月營收合併

import finlab
import pandas as pd
from finlab import data

pd.options.display.float_format = lambda x: "%.2f" % x


if __name__ == "__main__":
    close = data.get("price:收盤價")
    rev = data.get("monthly_revenue:當月營收")
    print(close)
    print(rev)

    merged_df = close.merge(rev, on='date', how='left', suffixes=('_close', '_rev'))
    merged_df.fillna(method='bfill', inplace=True)
    print(merged_df, merged_df.columns)
    print(merged_df['2330_close'], merged_df['2330_rev'])

臺股漲跌與市值板塊圖

出處: https://www.finlab.tw/dashboard2-plotly-treemap/

import pandas as pd
import numpy as np
import finlab
from finlab import data
import plotly.express as px


"""
https://www.finlab.tw/dashboard2-plotly-treemap/
Treemap
"""


def df_date_filter(df, start=None, end=None):
    if start:
        df = df[df.index >= start]
    if end:
        df = df[df.index <= end]
    return df


def create_treemap_data(start, end, item, clip=None):
    close = data.get("price:收盤價")
    basic_info = data.get("company_basic_info")
    turnover = data.get("price:成交金額")
    close_data = df_date_filter(close, start, end)
    turnover_data = df_date_filter(turnover, start, end).iloc[1:].sum() / 100000000
    return_ratio = (
        (close_data.iloc[-1] / close_data.iloc[-2]).dropna().replace(np.inf, 0)
    )
    return_ratio = round((return_ratio - 1) * 100, 2)

    concat_list = [close_data.iloc[-1], turnover_data, return_ratio]
    col_names = ["stock_id", "close", "turnover", "return_ratio"]
    if item not in ["return_ratio", "turnover_ratio"]:
        try:
            custom_item = df_date_filter(data.get(item), start, end).iloc[-1].fillna(0)
        except Exception as e:
            logger.error("data error, check the data is existed between start and end.")
            logger.error(e)
            return None
        if clip:
            custom_item = custom_item.clip(*clip)
        concat_list.append(custom_item)
        col_names.append(item)

    df = pd.concat(concat_list, axis=1).dropna()
    df = df.reset_index()
    df.columns = col_names

    basic_info_df = basic_info.copy()
    basic_info_df["stock_id_name"] = basic_info_df["stock_id"] + basic_info_df["公司簡稱"]

    df = df.merge(
        basic_info_df[["stock_id", "stock_id_name", "產業類別", "市場別", "實收資本額(元)"]],
        how="left",
        on="stock_id",
    )
    df = df.rename(columns={"產業類別": "category", "市場別": "market", "實收資本額(元)": "base"})
    df = df.dropna(thresh=5)
    df["market_value"] = round(df["base"] / 10 * df["close"] / 100000000, 2)
    df["turnover_ratio"] = df["turnover"] / (df["turnover"].sum()) * 100
    df["country"] = "TW-Stock"
    return df


def plot_tw_stock_treemap(
    start=None,
    end=None,
    area_ind="market_value",
    item="return_ratio",
    clip=None,
    color_scales="Temps",
):
    """Plot treemap chart for tw_stock

    Treemap charts visualize hierarchical data using nested rectangles,
    it is good for judging the overall market dynamics.

    Args:
      start(str): The date of data start point.ex:2021-01-02
      end(str):The date of data end point.ex:2021-01-05
      area_ind(str):The indicator to control treemap area size .
                    Select range is in ["market_value","turnover","turnover_ratio"]
      item(str): The indicator to control treemap area color .
                 Select range is in ["return_ratio", "turnover_ratio"]
                 or use the other customized data which you could find from finlab database page,
                 ex:'price_earning_ratio:本益比'
      clip(tuple):lower and upper pd.clip() setting for item values to make distinct colors.ex:(0,100)
      color_scales(str):Used for the built-in named continuous
                        (sequential, diverging and cyclical) color scales in Plotly
                        Ref:https://plotly.com/python/builtin-colorscales/
    Returns:
        figure
    """
    df = create_treemap_data(start, end, item, clip)
    if df is None:
        return None
    df["custom_item_label"] = round(df[item], 2).astype(str)

    if area_ind not in ["market_value", "turnover", "turnover_ratio"]:
        return None

    if item in ["return_ratio"]:
        color_continuous_midpoint = 0
    else:
        color_continuous_midpoint = np.average(df[item], weights=df[area_ind])

    fig = px.treemap(
        df,
        path=["country", "market", "category", "stock_id_name"],
        values=area_ind,
        color=item,
        color_continuous_scale=color_scales,
        color_continuous_midpoint=color_continuous_midpoint,
        custom_data=["custom_item_label", "close", "turnover"],
        title=f"TW-Stock Market TreeMap({start}~{end})"
        f"---area_ind:{area_ind}---item:{item}",
        width=1600,
        height=800,
    )

    fig.update_traces(
        textposition="middle center",
        textfont_size=24,
        texttemplate="%{label}(%{customdata[1]})<br>%{customdata[0]}",
    )
    return fig


if __name__ == "__main__":
    # @title 臺股漲跌與市值板塊圖
    start = "2021-07-01"  # @param {type:"date"}
    end = "2021-07-02"  # @param {type:"date"}
    area_ind = "turnover_ratio"  # @param ["market_value","turnover","turnover_ratio"] {allow-input: true}
    item = (
        "return_ratio"  # @param ["return_ratio", "turnover_ratio"] {allow-input: true}
    )
    clip = 1000  # @param {type:"number"}

    plot_tw_stock_treemap(start, end, area_ind, item, clip)

finlab 的 mae gmfe bmfe

以 2063 世鎧 mae 是 0 代表 2023-01-03 進場~ 2023-02-01 出場 的所有日K open 價都高過 進場當天開盤價

是以進場價格做基準點~ 藏獒策略以開盤價進場 開盤價出場

trade_indexstock_identry_dateexit_dateentry_sig_dateexit_sig_datereturntrade_price@entry_datetrade_price@exit_datemaegmfebmfemddreturn_include_fee
6302063 世鎧2023-01-032023-02-012022-12-302023-01-310.015943.900044.60000.00000.03420.0000-0.02871.0000
6313498 陽程2023-01-032023-02-012022-12-302023-01-31-0.022537.800036.9500-0.05420.05820.0582-0.1062-2.8200
6328996 高力2023-01-032023-02-012022-12-302023-01-310.1568185.0000214.0000-0.02700.15680.0270-0.052615.0000
6331104 環泥2023-02-012023-03-012023-01-312023-02-240.025323.700024.3000-0.00420.03160.0274-0.03081.9300
6341707 葡萄王2023-01-032023-03-012022-12-302023-02-240.0737169.5000182.0000-0.06190.07370.0000-0.06196.7500
6352727 王品2023-02-012023-03-012023-01-312023-02-240.4865185.0000275.00000.00000.48650.0000-0.045547.7800
6366612 奈米醫材2023-02-012023-03-012023-01-312023-02-240.2284116.0000142.50000.00000.34050.0000-0.087022.1300
6372916 滿心2023-03-012023-04-062023-02-242023-03-31-0.004632.800032.6500-0.02440.05490.0549-0.0751-1.0400
6383004 豐達科2023-03-012023-04-062023-02-242023-03-310.065089.200093.9000-0.06950.06500.0381-0.10374.6500
6393052 夆典2023-03-012023-04-062023-02-242023-03-31-0.056011.600010.9500-0.05600.00000.0000-0.0560-6.1500
6406664 群翊2023-03-012023-04-062023-02-242023-03-310.0648108.0000115.0000-0.06480.06480.0000-0.06485.8600
6418931 大汽電2023-02-012023-04-062023-01-312023-03-310.628246.800076.20000.00000.72860.0000-0.058161.8700
6423078 僑威2023-04-06nan2023-03-312023-04-300.236244.250054.7000-0.01470.31070.0000-0.056922.8900
6433540 曜越2023-04-06nan2023-03-312023-04-300.077338.800041.8000-0.00520.15210.0013-0.06497.1000
6444119 旭富2023-04-06nan2023-03-312023-04-300.0496121.0000127.00000.00000.09920.0000-0.04514.3500
6454153 鈺緯2023-04-06nan2023-03-312023-04-300.100048.000052.80000.00000.26250.0000-0.12879.3600
6464190 佐登-KY2023-04-06nan2023-03-312023-04-300.074194.5000101.5000-0.01690.11110.0000-0.03336.7800
# -*- coding: utf-8 -*-
"""correlationMatrix.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/1jmq3ycgp65_NURP8cY3vg2SvTDXc5X8n
"""

#!pip install yfinance > log.txt
#@title 輸入 Yahoo 股票代號(例如: 2330.TW, AAPL, BTC-USD)
stock_ids = "2454.TW,2330.TW, AAPL, BTC-USD" #@param {type:"string"}

import yfinance as yf
import time
import pandas as pd

stocks = stock_ids.replace(' ', '').split(',')
price = {}

for s in stocks:

  print(f'download {s}')

  ss = yf.Ticker(s)

  # get historical market data
  hist = ss.history(period='1y')

  price[s] = hist['Close']
  time.sleep(3)

import seaborn
import matplotlib.pyplot as plt

plt.rcParams['figure.figsize'] = (10, 6)
seaborn.heatmap(pd.DataFrame(price).pct_change().dropna(how='any').corr(), cmap="YlGnBu",vmax=1,vmin=-1, annot=True)

finlab策略

from finlab import data
from finlab.backtest import sim
import pandas as pd
import redis
import finlab

def connect_redis():
    r = None
    pool = redis.ConnectionPool(host="localhost", port=6379, db=8)
    try:
        r = redis.Redis(connection_pool=pool, charset="utf-8")
    except Exception as err:
        logger.error(err)
    return r

# rs = connect_redis()
# rs.flushdb()

finlab.login("")
score = data.get('etl:finlab_tw_stock_market_ind')['score']
close = data.get("price:收盤價")
vol = data.get("price:成交股數")
vol_ma = vol.average(10)
rev = data.get('monthly_revenue:當月營收')
rev_year_growth = data.get('monthly_revenue:去年同月增減(%)')
rev_month_growth = 	data.get('monthly_revenue:上月比較增減(%)')

# 股價創年新高
cond1 = (close == close.rolling(250).max())

# 排除月營收連3月衰退10%以上
cond2 = ~(rev_year_growth < -10).sustain(3) 

# 排除月營收成長趨勢過老(12個月內有至少8個月單月營收年增率大於60%)
cond3 = ~(rev_year_growth > 60).sustain(12,8) 

# 確認營收底部,近月營收脫離近年穀底(連續3月的"單月營收近12月最小值/近月營收" < 0.8)
cond4 = ((rev.rolling(12).min())/(rev) < 0.8).sustain(3)

# 單月營收月增率連續3月大於-40%
cond5 = (rev_month_growth > -40).sustain(3)

# 流動性條件
cond6 = vol_ma > 200*1000

buy = cond1 & cond2  & cond3 & cond4 & cond5 & cond6

# 買比較冷門的股票
buy = vol_ma*buy
buy = buy[buy>0]
buy = buy.is_smallest(5)
long_position = buy.resample('M').last().reindex(close.index,method='ffill')

score_df = score >= 4
long_position *= score_df

# 做空訊號~多單遇大盤訊號轉空時出場,並反手做空指數避險
short_target = '00632R'
short_position = close[[short_target]].notna() * ~score_df
position = pd.concat([long_position, short_position], axis=1)

report = sim(position, upload=True, position_limit=1/3, fee_ratio=1.425/1000/3, stop_loss=0.08,  trade_at_price='open' ,name='XXXX', live_performance_start='2022-06-01')
print(report.get_trades().to_markdown())

from finlab import data
from finlab.backtest import sim
import finlab

finlab.login("")

close = data.get("price:收盤價")
vol = data.get("price:成交股數")
vol_ma = vol.average(10)
rev = data.get('monthly_revenue:當月營收')
rev_year_growth = data.get('monthly_revenue:去年同月增減(%)')
rev_month_growth = 	data.get('monthly_revenue:上月比較增減(%)')

# 股價創年新高
cond1 = (close == close.rolling(250).max())

# 排除月營收連3月衰退10%以上
cond2 = ~(rev_year_growth < -10).sustain(3) 

# 排除月營收成長趨勢過老(12個月內有至少8個月單月營收年增率大於60%)
cond3 = ~(rev_year_growth > 60).sustain(12,8)

# 確認營收底部(單月營收月增率連續3月大於-40)
cond4 = ((rev.rolling(12).min())/(rev) < 0.8).sustain(3)

# 單月營收月增率連續3月大於-40%
cond5 = (rev_month_growth > -40).sustain(3)

# 流動性條件
cond6 = vol_ma > 200*1000

buy = cond1 & cond2  & cond3 & cond4 & cond5 & cond6

# 買比較冷門的股票
buy = vol_ma*buy
buy = buy[buy>0]
buy = buy.is_smallest(5)

report = sim(buy , resample="M", upload=True, position_limit=1/3, fee_ratio=1.425/1000/3, stop_loss=0.08,  trade_at_price='open',name='XXX', live_performance_start='2022-05-01')
print(report.get_trades().to_markdown())

from loguru import logger
from finlab import data
from finlab.backtest import sim
import finlab
import pandas as pd
import pickle
import redis
import zlib


def data_to_redis(r):
    score = data.get("etl:finlab_tw_stock_market_ind")["score"]
    close = data.get("price:收盤價")
    vol = data.get("price:成交股數")
    vol_ma = vol.average(10)
    rev = data.get("monthly_revenue:當月營收")
    rev_year_growth = data.get("monthly_revenue:去年同月增減(%)")
    rev_month_growth = data.get("monthly_revenue:上月比較增減(%)")
    EXPIRATION_SECONDS = 86400
    # Set
    r.setex("score", EXPIRATION_SECONDS, zlib.compress(pickle.dumps(score)))
    r.setex("close", EXPIRATION_SECONDS, zlib.compress(pickle.dumps(close)))
    r.setex("vol", EXPIRATION_SECONDS, zlib.compress(pickle.dumps(vol)))
    r.setex("vol_ma", EXPIRATION_SECONDS, zlib.compress(pickle.dumps(vol_ma)))
    r.setex("rev", EXPIRATION_SECONDS, zlib.compress(pickle.dumps(rev)))
    r.setex(
        "rev_year_growth",
        EXPIRATION_SECONDS,
        zlib.compress(pickle.dumps(rev_year_growth)),
    )
    r.setex(
        "rev_month_growth",
        EXPIRATION_SECONDS,
        zlib.compress(pickle.dumps(rev_month_growth)),
    )

def backtest(r):
    # Get
    score_df = pickle.loads(zlib.decompress(r.get("score")))
    close_df = pickle.loads(zlib.decompress(r.get("close")))
    vol_df = pickle.loads(zlib.decompress(r.get("vol")))
    vol_ma_df = pickle.loads(zlib.decompress(r.get("vol_ma")))
    rev_df = pickle.loads(zlib.decompress(r.get("rev")))
    rev_year_growth_df = pickle.loads(zlib.decompress(r.get("rev_year_growth")))
    rev_month_growth_df = pickle.loads(zlib.decompress(r.get("rev_month_growth")))
    # print(score_df)
    # print(close_df)
    # print(vol_df)
    # print(vol_ma_df)
    # print(rev_df)
    # print(rev_year_growth_df)
    # print(rev_month_growth_df)
    # 股價創年新高
    cond1 = close_df == close_df.rolling(250).max()

    # 排除月營收連3月衰退10%以上
    cond2 = ~(rev_year_growth_df < -10).sustain(3)

    # 排除月營收成長趨勢過老(12個月內有至少8個月單月營收年增率大於60%)
    cond3 = ~(rev_year_growth_df > 60).sustain(12, 8)

    # 確認營收底部,近月營收脫離近年穀底(連續3月的"單月營收近12月最小值/近月營收" < 0.8)
    cond4 = ((rev_df.rolling(12).min()) / (rev_df) < 0.8).sustain(3)

    # 單月營收月增率連續3月大於-40%
    cond5 = (rev_month_growth_df > -40).sustain(3)

    # 流動性條件
    cond6 = vol_ma_df > 200 * 1000
    buy = cond1 & cond2 & cond3 & cond4 & cond5 & cond6

    # 買比較冷門的股票
    buy = vol_ma_df * buy
    buy = buy[buy > 0]
    buy = buy.is_smallest(5)
    long_position = buy.resample("M").last().reindex(close_df.index, method="ffill")
    score_df = score_df >= 4
    long_position *= score_df

    # 做空訊號~多單遇大盤訊號轉空時出場,並反手做空指數避險
    short_target = "00632R"
    short_position = close_df[[short_target]].notna() * ~score_df
    position = pd.concat([long_position, short_position], axis=1)

    report = sim(
        position,
        upload=True,
        position_limit=1 / 3,
        fee_ratio=1.425 / 1000 / 3,
        stop_loss=0.08,
        trade_at_price="open",
        name="XXXXX",
        live_performance_start="2022-06-01",
    )
    # print(report.get_stats())


def connect_redis():
    pool = redis.ConnectionPool(host="localhost", port=6379, db=0)
    try:
        r = redis.Redis(connection_pool=pool, charset="utf-8")
    except Exception as err:
        logger.error(err)
    return r


if __name__ == "__main__":
    finlab.login(
        ""
    )
    r = connect_redis()
    # data_to_redis(r)
    backtest(r)

highcharts_股價走勢.ipynb

https://colab.research.google.com/drive/1W1kH3cwNUTj7hMMyF8W4wcehiWLSyAUF?usp=sharing#scrollTo=Mij5sRmwbtCP

import yfinance as yf

# 取得股價歷史資料(含臺股\美股\加密貨幣)

symbol = '2330.TW' # 臺股上市:TW 臺股上櫃:TWO 
start = '2018-01-01' # 起始時間
end = '2022-12-31' # 結束時間

ohlcv = yf.Ticker(symbol).history('max').loc[start:end]


from highcharts import Highchart
import datetime
from IPython.display import HTML,display
import os

# 客製化調整參數
color = '#4285f4' # 線的顏色 (red/green/blue/purple)
linewidth = 2 # 線的粗細
title = symbol # 標題名稱
width = 800 # 圖的寬度
height = 500 # 圖的高度


# 繪圖設定
H = Highchart(width=width,height=height)

x = ohlcv.index
y = round(ohlcv.Close,2)

data = [[index,s] for index,s in zip(x,y)]
H.add_data_set(data,'line','data',color=color)

H.set_options('xAxis',{'type':'datetime'})
H.set_options('title',{'text':title,'style':{'color':'black'}}) # 設定title
H.set_options('plotOptions',{'line':{'lineWidth':linewidth,'dataLabels':{'enabled': False}}}) # 設定線的粗度
H.set_options('tooltip',{'shared':True,'crosshairs':True}) # 設定為可互動式

# 顯示圖表
H.save_file('chart')
display(HTML('chart.html'))
os.remove('chart.html')

突破策略豆知識 | 如何避免假突破?

https://colab.research.google.com/drive/1M0XxnAMZoqoOrJQP9dyJVer5Q7YyRFOA?usp=sharing

https://www.finlab.tw/breakthrough_stock_picking_strategies/

# -*- coding: utf-8 -*-
"""股價創新高動能.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/1M0XxnAMZoqoOrJQP9dyJVer5Q7YyRFOA

## 安裝套件
"""

# Commented out IPython magic to ensure Python compatibility.
# %%capture
# !pip install finlab > log.txt
# !pip install talib-binary > log.txt

"""## 股價創新高動能"""

from finlab.backtest import sim
from finlab import data

# 標的範圍為上市櫃普通股
with data.universe(market='TSE_OTC'):
    # 取得收盤價
    close = data.get("price:收盤價")
    # 股價創近200日新高
    position = (close == close.rolling(200).max())
    # 每兩週再平衡,單檔最大持股比例限制20%,停損20%
    report = sim(position, resample="2W", position_limit=0.2, stop_loss=0.2, name="股價創新高策略", upload=False)
    report.display()

"""## 創新高延續動能策略

"""

from finlab.backtest import sim
from finlab import data
with data.universe(market='TSE_OTC'):
    close = data.get("price:收盤價")
    # 近5日內有3日以上的股價創前200日新高
    position = (close == close.rolling(200).max()).sustain(5,3)
    report = sim(position, resample="2W", position_limit=0.2, stop_loss=0.2, name="創新高延續動能策略", upload=False)
    report.display()
import yfinance as yf

# 下載臺積電股票資料
df = yf.download("2317.TW", start="2014-01-01", end="2023-01-01")

# 將時間單位轉換為月份,取得每個月份的最後一筆資料,並填補缺失值
df = df.resample("M").last().bfill()

# 根據原始資料的時間索引重新排序,並填補缺失值
df = df.reindex(df.index, method="bfill")

print(df)
  • FinlabDataFrame
from finlab.utils import logger
import datetime
import numpy as np
import pandas as pd
from finlab import data
import functools


class FinlabDataFrame(pd.DataFrame):
    """回測語法糖
    除了使用熟悉的 Pandas 語法外,我們也提供很多語法糖,讓大家開發程式時,可以用簡易的語法完成複雜的功能,讓開發策略更簡潔!
    我們將所有的語法糖包裹在 `FinlabDataFrame` 中,用起來跟 `pd.DataFrame` 一樣,但是多了很多功能!
    只要使用 `finlab.data.get()` 所獲得的資料,皆為 `FinlabDataFrame` 格式,
    接下來我們就來看看, `FinlabDataFrame` 有哪些好用的語法糖吧!

    當資料日期沒有對齊(例如: 財報 vs 收盤價 vs 月報)時,在使用以下運算符號:`+`, `-`, `*`, `/`, `>`, `>=`, `==`, `<`, `<=`, `&`, `|`, `~`,不需要先將資料對齊,因為 `FinlabDataFrame` 會自動幫你處理,以下是示意圖。

    <img src="https://i.ibb.co/pQr5yx5/Screen-Shot-2021-10-26-at-5-32-44-AM.png" alt="Screen-Shot-2021-10-26-at-5-32-44-AM">

    以下是範例:`cond1` 與 `cond2` 分別為「每天」,和「每季」的資料,假如要取交集的時間,可以用以下語法:

    ```py
    from finlab import data
    # 取得 FinlabDataFrame
    close = data.get('price:收盤價')
    roa = data.get('fundamental_features:ROA稅後息前')

    # 運算兩個選股條件交集
    cond1 = close > 37
    cond2 = roa > 0
    cond_1_2 = cond1 & cond2
擷取 1101 臺泥 的訊號如下圖,可以看到 `cond1` 跟 `cond2` 訊號的頻率雖然不相同,但是由於 `cond1` 跟 `cond2` 是 `FinlabDataFrame`,所以可以直接取交集,而不用處理資料頻率對齊的問題。
<br />
<img src="https://i.ibb.co/m9chXSQ/imageconds.png" alt="imageconds">

總結來說,FinlabDataFrame 與一般 dataframe 唯二不同之處:
1. 多了一些 method,如`df.is_largest()`, `df.sustain()`...等。
2. 在做四則運算、不等式運算前,會將 df1、df2 的 index 取聯集,column 取交集。
"""

@property
def _constructor(self):
    return FinlabDataFrame

@staticmethod
def reshape(df1, df2):

    isfdf1 = isinstance(df1, FinlabDataFrame)
    isfdf2 = isinstance(df2, FinlabDataFrame)
    isdf1 = isinstance(df1, pd.DataFrame)
    isdf2 = isinstance(df2, pd.DataFrame)

    both_are_dataframe = (isfdf1 + isdf1) * (isfdf2 + isdf2) != 0

    d1_index_freq = df1.get_index_str_frequency() if isfdf1 else None
    d2_index_freq = df2.get_index_str_frequency() if isfdf2 else None

    if ((d1_index_freq or d2_index_freq)
      and (d1_index_freq != d2_index_freq)
      and both_are_dataframe):

        df1 = df1.index_str_to_date() if isfdf1 else df1
        df2 = df2.index_str_to_date() if isfdf2 else df2

    if isinstance(df2, pd.Series):
        df2 = pd.DataFrame({c: df2 for c in df1.columns})

    if both_are_dataframe:
        index = df1.index.union(df2.index)
        columns = df1.columns.intersection(df2.columns)

        if len(df1.index) * len(df2.index) != 0:
          index_start = max(df1.index[0], df2.index[0])
          index = [t for t in index if index_start <= t]

        return df1.reindex(index=index, method='ffill')[columns], \
            df2.reindex(index=index, method='ffill')[columns]
    else:
        return df1, df2

def __lt__(self, other):
    df1, df2 = self.reshape(self, other)
    return pd.DataFrame.__lt__(df1, df2)

def __gt__(self, other):
    df1, df2 = self.reshape(self, other)
    return pd.DataFrame.__gt__(df1, df2)

def __le__(self, other):
    df1, df2 = self.reshape(self, other)
    return pd.DataFrame.__le__(df1, df2)

def __ge__(self, other):
    df1, df2 = self.reshape(self, other)
    return pd.DataFrame.__ge__(df1, df2)

def __eq__(self, other):
    df1, df2 = self.reshape(self, other)
    return pd.DataFrame.__eq__(df1, df2)

def __ne__(self, other):
    df1, df2 = self.reshape(self, other)
    return pd.DataFrame.__ne__(df1, df2)

def __sub__(self, other):
    df1, df2 = self.reshape(self, other)
    return pd.DataFrame.__sub__(df1, df2)

def __add__(self, other):
    df1, df2 = self.reshape(self, other)
    return pd.DataFrame.__add__(df1, df2)

def __mul__(self, other):
    df1, df2 = self.reshape(self, other)
    return pd.DataFrame.__mul__(df1, df2)

def __truediv__(self, other):
    df1, df2 = self.reshape(self, other)
    return pd.DataFrame.__truediv__(df1, df2)

def __rshift__(self, other):
    return self.shift(-other)

def __lshift__(self, other):
    return self.shift(other)

def __and__(self, other):
    df1, df2 = self.reshape(self, other)
    return pd.DataFrame.__and__(df1, df2)

def __or__(self, other):
    df1, df2 = self.reshape(self, other)
    return pd.DataFrame.__or__(df1, df2)

def __getitem__(self, other):
    df1, df2 = self.reshape(self, other)
    return pd.DataFrame.__getitem__(df1, df2)

def index_str_to_date(self):
  """財務月季報索引格式轉換

    將以下資料的索引轉換成datetime格式:

    月營收 (ex:2022-M1) 從文字格式轉為公告截止日。

    財務季報 (ex:2022-Q1) 從文字格式轉為財報電子檔資料上傳日。

    通常使用情境為對不同週期的dataframe做reindex,常用於以公告截止日作為訊號產生日。

    Returns:
      (pd.DataFrame): data
    Examples:

        ```py
        data.get('monthly_revenue:當月營收').index_str_to_date()
        data.get('financial_statement:現金及約當現金').index_str_to_date()
        ```
  """
  if len(self.index) == 0 or not isinstance(self.index[0], str):
    return self

  if self.index[0].find('M') != -1:
    return self._index_str_to_date_month()
  elif self.index[0].find('Q') != -1:
    return self._index_str_to_date_season()

  return self

@staticmethod
def to_business_day(date):

    def skip_weekend(d):
        add_days = {5: 2, 6: 1}
        wd = d.weekday()
        if wd in add_days: d += datetime.timedelta(days=add_days[wd])
        return d

    close = data.get('price:收盤價')
    return pd.Series(date).apply(lambda d: skip_weekend(d) if d in close.index or d < close.index[0] or d > close.index[-1] else close.loc[d:].index[0]).values

def get_index_str_frequency(self):

    if len(self.index) == 0:
      return None

    if not isinstance(self.index[0], str):
      return None

    if (self.index.str.find('M') != -1).all():
      return 'month'

    if (self.index.str.find('Q') != -1).all():
      return 'season'

    return None

def _index_date_to_str_month(self):

    # index is already str
    if len(self.index) == 0 or not isinstance(self.index[0], pd.Timestamp):
      return self

    index = (self.index - datetime.timedelta(days=30)).strftime('%Y-M%m')
    return FinlabDataFrame(self.values, index=index, columns=self.columns)

def _index_str_to_date_month(self):

    # index is already timestamps
    if len(self.index) == 0 or not isinstance(self.index[0], str):
      return self

    if not (self.index.str.find('M') != -1).all():
      logger.warning('FinlabDataFrame: invalid index, cannot format index to monthly timestamp.')
      return self

    index = pd.to_datetime(self.index, format='%Y-M%m') + pd.offsets.MonthBegin() + datetime.timedelta(days=9)
    # chinese new year and covid-19 impact monthly revenue deadline
    replacements = {
                    datetime.datetime(2020, 2, 10): datetime.datetime(2020, 2, 15),
                    datetime.datetime(2021, 2, 10): datetime.datetime(2021, 2, 15),
                    datetime.datetime(2022, 2, 10): datetime.datetime(2022, 2, 14),
                    }
    replacer = replacements.get
    index = [replacer(n, n) for n in index]

    index = self.to_business_day(index)

    ret = FinlabDataFrame(self.values, index=index, columns=self.columns)
    ret.index.name = 'date'
 
    return ret
def _index_date_to_str_season(self):

    # index is already str
    if len(self.index) == 0 or not isinstance(self.index[0], pd.Timestamp):
      return self

    q = self.index.strftime('%m').astype(int).map({5:1, 8:2, 9:2, 10:3, 11:3, 3:4, 4:4})
    year = self.index.year.copy()
    year -= (q == 4)
    index = year.astype(str) + '-Q' + q.astype(str)

    return FinlabDataFrame(self.values, index=index, columns=self.columns)

def deadline(self):
    """財務季報索引轉換成公告截止日

      將財務季報 (ex:2022Q1) 從文字格式轉為公告截止日的datetime格式,
      通常使用情境為對不同週期的dataframe做reindex,常用於以公告截止日作為訊號產生日。
      Returns:
        (pd.DataFrame): data
      Examples:
          ```py
          data.get('financial_statement:現金及約當現金').deadline()
          ```
    """
    return self._index_str_to_date_season(detail=False)

def _index_str_to_date_season(self, detail=True):

  disclosure_dates = (calc_disclosure_dates(detail)
                      .reindex_like(self)
                      .unstack())

  self.columns.name = 'stock_id'

  unstacked = self.unstack()

  ret = (pd.DataFrame({
      'value': unstacked.values,
      'disclosures': disclosure_dates.values,
    }, unstacked.index)
    .reset_index()
    .drop_duplicates(['disclosures', 'stock_id'])
    .pivot(index='disclosures', columns='stock_id', values='value').ffill()
    .pipe(lambda df: df.loc[df.index.notna()])
    .pipe(lambda df: FinlabDataFrame(df))
    .rename_axis('date')
  )

  if not detail:
      ret.index = self.to_business_day(ret.index)

  return ret

def average(self, n):
    """取 n 筆移動平均

    若股票在時間窗格內,有 N/2 筆 NaN,則會產生 NaN。
    Args:
      n (positive-int): 設定移動窗格數。
    Returns:
      (pd.DataFrame): data
    Examples:
        股價在均線之上
        ```py
        from finlab import data
        close = data.get('price:收盤價')
        sma = close.average(10)
        cond = close > sma
        ```
        只需要簡單的語法,就可以將其中一部分的訊號繪製出來檢查:
        ```py
        import matplotlib.pyplot as plt

        close.loc['2021', '2330'].plot()
        sma.loc['2021', '2330'].plot()
        cond.loc['2021', '2330'].mul(20).add(500).plot()

        plt.legend(['close', 'sma', 'cond'])
        ```
        <img src="https://i.ibb.co/Mg1P85y/sma.png" alt="sma">
    """
    return self.rolling(n, min_periods=int(n/2)).mean()

def is_largest(self, n):
    """取每列前 n 筆大的數值

    若符合 `True` ,反之為 `False` 。用來篩選每天數值最大的股票。

    <img src="https://i.ibb.co/8rh3tbt/is-largest.png" alt="is-largest">
    Args:
      n (positive-int): 設定每列前 n 筆大的數值。
    Returns:
      (pd.DataFrame): data
    Examples:
        每季 ROA 前 10 名的股票
        ```py
        from finlab import data

        roa = data.get('fundamental_features:ROA稅後息前')
        good_stocks = roa.is_largest(10)
        ```
    """
    return self.astype(float).apply(lambda s: s.nlargest(n), axis=1).reindex_like(self).notna()

def is_smallest(self, n):
    """取每列前 n 筆小的數值

    若符合 `True` ,反之為 `False` 。用來篩選每天數值最小的股票。
    Args:
      n (positive-int): 設定每列前 n 筆小的數值。
    Returns:
      (pd.DataFrame): data
    Examples:
        股價淨值比最小的 10 檔股票
        ```py
        from finlab import data

        pb = data.get('price_earning_ratio:股價淨值比')
        cheap_stocks = pb.is_smallest(10)
        ```
    """
    return self.astype(float).apply(lambda s: s.nsmallest(n), axis=1).reindex_like(self).notna()

def is_entry(self):
    """進場點

    取進場訊號點,若符合條件的值則為True,反之為False。
    Returns:
      (pd.DataFrame): data
    Examples:
      策略為每日收盤價前10高,取進場點。
        ```py
        from finlab import data
        data.get('price:收盤價').is_largest(10).is_entry()
        ```
    """
    return (self & ~self.shift(fill_value=False))

def is_exit(self):
    """出場點

    取出場訊號點,若符合條件的值則為 True,反之為 False。
    Returns:
      (pd.DataFrame): data
    Examples:
      策略為每日收盤價前10高,取出場點。
        ```py
        from finlab import data
        data.get('price:收盤價').is_largest(10).is_exit()
        ```
    """
    return (~self & self.shift(fill_value=False))

def rise(self, n=1):
    """數值上升中

    取是否比前第n筆高,若符合條件的值則為True,反之為False。
    <img src="https://i.ibb.co/Y72bN5v/Screen-Shot-2021-10-26-at-6-43-41-AM.png" alt="Screen-Shot-2021-10-26-at-6-43-41-AM">
    Args:
      n (positive-int): 設定比較前第n筆高。
    Returns:
      (pd.DataFrame): data
    Examples:
        收盤價是否高於10日前股價
        ```py
        from finlab import data
        data.get('price:收盤價').rise(10)
        ```
    """
    return self > self.shift(n)

def fall(self, n=1):
    """數值下降中

    取是否比前第n筆低,若符合條件的值則為True,反之為False。
    <img src="https://i.ibb.co/Y72bN5v/Screen-Shot-2021-10-26-at-6-43-41-AM.png" alt="Screen-Shot-2021-10-26-at-6-43-41-AM">
    Args:
      n (positive-int): 設定比較前第n筆低。
    Returns:
      (pd.DataFrame): data
    Examples:
        收盤價是否低於10日前股價
        ```py
        from finlab import data
        data.get('price:收盤價').fall(10)
        ```
    """
    return self < self.shift(n)

def groupby_category(self):
    """資料按產業分群

    類似 `pd.DataFrame.groupby()`的處理效果。
    Returns:
      (pd.DataFrame): data
    Examples:
      半導體平均股價淨值比時間序列
        ```py
        from finlab import data
        pe = data.get('price_earning_ratio:股價淨值比')
        pe.groupby_category().mean()['半導體'].plot()
        ```
        <img src="https://i.ibb.co/Tq2fKBp/pbmean.png" alt="pbmean">

        全球 2020 量化寬鬆加上晶片短缺,使得半導體股價淨值比衝高。
    """
    categories = data.get('security_categories')
    cat = categories.set_index('stock_id').category.to_dict()
    org_set = set(cat.values())
    set_remove_illegal = set(
        o for o in org_set if isinstance(o, str) and o != 'nan')
    set_remove_illegal

    refine_cat = {}
    for s, c in cat.items():
        if c == None or c == 'nan':
            refine_cat[s] = '其他'
            continue

        if c == '電腦及週邊':
            refine_cat[s] = '電腦及週邊設備業'
            continue

        if c[-1] == '業' and c[:-1] in set_remove_illegal:
            refine_cat[s] = c[:-1]
        else:
            refine_cat[s] = c

    col_categories = pd.Series(self.columns.map(
        lambda s: refine_cat[s] if s in cat else '其他'))

    return self.groupby(col_categories.values, axis=1)

def entry_price(self, trade_at='close'):

    signal = self.is_entry()
    adj = data.get('etl:adj_close') if trade_at == 'close' else data.get(
        'etl:adj_open')
    adj, signal = adj.reshape(
        adj.loc[signal.index[0]: signal.index[-1]], signal)
    return adj.bfill()[signal.shift(fill_value=False)].ffill()

def sustain(self, nwindow, nsatisfy=None):
    """持續 N 天滿足條件

    取移動 nwindow 筆加總大於等於nsatisfy,若符合條件的值則為True,反之為False。

    Args:
      nwindow (positive-int): 設定移動窗格。
      nsatisfy (positive-int): 設定移動窗格計算後最低滿足數值。
    Returns:
      (pd.DataFrame): data
    Examples:
        收盤價是否連兩日上漲
        ```py
        from finlab import data
        data.get('price:收盤價').rise().sustain(2)
        ```
    """
    nsatisfy = nsatisfy or nwindow
    return self.rolling(nwindow).sum() >= nsatisfy

def industry_rank(self, categories=None):
    """計算產業 ranking 排名,0 代表產業內最低,1 代表產業內最高
    Args:
      categories (list of str): 欲考慮的產業,ex: ['貿易百貨', '雲端運算'],預設為全產業,請參考 `data.get('security_industry_themes')` 中的產業項目。
    Examples:
        本意比產業排名分數
        ```py
        from finlab import data

        pe = data.get('price_earning_ratio:本益比')
        pe_rank = pe.industry_rank()
        print(pe_rank)
        ```
    """

    themes = (data.get('security_industry_themes')
        .copy() # 複製
        .assign(category=lambda self: self.category
            .apply(lambda s: eval(s))) # 從文字格式轉成陣列格
        .explode('category') # 展開資料
    )

    categories = (categories 
        or set(themes.category[themes.category.str.find(':') == -1]))

    def calc_rank(ind):
        stock_ids = themes.stock_id[themes.category == ind]
        return (self[stock_ids].pipe(lambda self: self.rank(axis=1, pct=True)))

    return (pd.concat([calc_rank(ind) for ind in categories],axis=1)
        .groupby(level=0, axis=1).mean())

def quantile_row(self, c):
    """股票當天數值分位數

    取得每列c定分位數的值。
    Args:
      c (positive-int): 設定每列 n 定分位數的值。
    Returns:
      (pd.DataFrame): data
    Examples:
        取每日股價前90%分位數
        ```py
        from finlab import data
        data.get('price:收盤價').quantile_row(0.9)
        ```
    """
    s = self.quantile(c, axis=1)
    return s

def exit_when(self, exit):

    df, exit = self.reshape(self, exit)

    df.fillna(False, inplace=True)
    exit.fillna(False, inplace=True)

    entry_signal = df.is_entry()
    exit_signal = df.is_exit()
    exit_signal |= exit

    # build position using entry_signal and exit_signal
    position = pd.DataFrame(np.nan, index=df.index, columns=df.columns)
    position[entry_signal] = 1
    position[exit_signal] = 0

    position.ffill(inplace=True)
    position = position == 1
    position.fillna(False)
    return position

def hold_until(self, exit, nstocks_limit=None, stop_loss=-np.inf, take_profit=np.inf, trade_at='close', rank=None):
    """訊號進出場

    這大概是所有策略撰寫中,最重要的語法糖,上述語法中 `entries` 為進場訊號,而 `exits` 是出場訊號。所以 `entries.hold_until(exits)` ,就是進場訊號為 `True` 時,買入並持有該檔股票,直到出場訊號為 `True ` 則賣出。
    <img src="https://i.ibb.co/PCt4hPd/Screen-Shot-2021-10-26-at-6-35-05-AM.png" alt="Screen-Shot-2021-10-26-at-6-35-05-AM">
    此函式有很多細部設定,可以讓你最多選擇 N 檔股票做輪動。另外,當超過 N 檔進場訊號發生,也可以按照客製化的排序,選擇優先選入的股票。最後,可以設定價格波動當輪動訊號,來增加出場的時機點。

    Args:
      exit (pd.Dataframe): 出場訊號。
      nstocks_limit (int)`: 輪動檔數上限,預設為None。
      stop_loss (float): 價格波動輪動訊號,預設為None,不生成輪動訊號。範例:0.1,代表成本價下跌 10% 時產生出場訊號。
      take_profit (float): 價格波動輪動訊號,預設為None,不生成輪動訊號。範例:0.1,代表成本價上漲 10% 時產生出場訊號。
      trade_at (str): 價格波動輪動訊號參考價,預設為'close'。可選 `close` 或 `open`。
      rank (pd.Dataframe): 當天進場訊號數量超過 nstocks_limit 時,以 rank 數值越大的股票優先進場。
    Returns:
      (pd.DataFrame): data
    Examples:
        價格 > 20 日均線入場, 價格 < 60 日均線出場,最多持有10檔,超過 10 個進場訊號,則以股價淨值比小的股票優先選入。

        ```python
        from finlab import data
        from finlab.backtest import sim

        close = data.get('price:收盤價')
        pb = data.get('price_earning_ratio:股價淨值比')

        sma20 = close.average(20)
        sma60 = close.average(60)

        entries = close > sma20
        exits = close < sma60

        #pb前10小的標的做輪動
        position = entries.hold_until(exits, nstocks_limit=10, rank=-pb)
        sim(position)
        ```
    """
    if nstocks_limit is None:
        nstocks_limit = len(self.columns)

    union_index = self.index.union(exit.index)
    intersect_col = self.columns.intersection(exit.columns)

    if stop_loss != -np.inf or take_profit != np.inf:
        price = data.get(f'etl:adj_{trade_at}')
        union_index = union_index.union(
            price.loc[union_index[0]: union_index[-1]].index)
        intersect_col = intersect_col.intersection(price.columns)
    else:
        price = pd.DataFrame()

    if rank is not None:
        union_index = union_index.union(rank.index)
        intersect_col = intersect_col.intersection(rank.columns)

    entry = self.reindex(union_index, columns=intersect_col,
                         method='ffill').ffill().fillna(False)
    exit = exit.reindex(union_index, columns=intersect_col,
                        method='ffill').ffill().fillna(False)

    if price is not None:
        price = price.reindex(
            union_index, columns=intersect_col, method='ffill')

    if rank is not None:
        rank = rank.reindex(
            union_index, columns=intersect_col, method='ffill')
    else:
        rank = pd.DataFrame(1, index=union_index, columns=intersect_col)

    max_rank = rank.max().max()
    min_rank = rank.min().min()
    rank = (rank - min_rank) / (max_rank - min_rank)
    rank.fillna(0, inplace=True)

    def rotate_stocks(ret, entry, exit, nstocks_limit, stop_loss=-np.inf, take_profit=np.inf, price=None, ranking=None):

        nstocks = 0

        ret[0][np.argsort(entry[0])[-nstocks_limit:]] = 1
        ret[0][exit[0] == 1] = 0
        ret[0][entry[0] == 0] = 0

        entry_price = np.empty(entry.shape[1])
        entry_price[:] = np.nan

        for i in range(1, entry.shape[0]):

            # regitser entry price
            if stop_loss != -np.inf or take_profit != np.inf:
                is_entry = ((ret[i-2] == 0) if i >
                            1 else (ret[i-1] == 1))

                is_waiting_for_entry = np.isnan(entry_price) & (ret[i-1] == 1)

                is_entry |= is_waiting_for_entry

                entry_price[is_entry == 1] = price[i][is_entry == 1]

                # check stop_loss and take_profit
                returns = price[i] / entry_price
                stop = (returns > 1 + abs(take_profit)
                        ) | (returns < 1 - abs(stop_loss))
                exit[i] |= stop

            # run signal
            rank = (entry[i] * ranking[i] + ret[i-1] * 3)
            rank[exit[i] == 1] = -1
            rank[(entry[i] == 0) & (ret[i-1] == 0)] = -1

            ret[i][np.argsort(rank)[-nstocks_limit:]] = 1
            ret[i][rank == -1] = 0

        return ret

    ret = pd.DataFrame(0, index=entry.index, columns=entry.columns)
    ret = rotate_stocks(ret.values,
                        entry.astype(int).values,
                        exit.astype(int).values,
                        nstocks_limit,
                        stop_loss,
                        take_profit,
                        price=price.values,
                        ranking=rank.values)

    return pd.DataFrame(ret, index=entry.index, columns=entry.columns)

@functools.lru_cache def calc_disclosure_dates(detail=True):

cinfo = data.get('company_basic_info').copy() cinfo['id'] = cinfo.stock_id.str.split(' ').str[0] cinfo = cinfo.set_index('id') cinfo = cinfo[~cinfo.index.duplicated(keep='last')]

def calc_default_disclosure_dates(s): sid = s.name cat = cinfo.loc[sid].產業類別 if sid in cinfo.index else 'etf' short_name = cinfo.loc[sid].公司簡稱 if sid in cinfo.index else 'etf'

if cat == '金融業':
  calendar = {
    '1': '-05-15',
    '2': '-08-31',
    '3': '-11-14',
    '4': '-03-31',
  }
elif cat == '金融保險業':
  calendar = {
    '1': '-04-30',
    '2': '-08-31',
    '3': '-10-31',
    '4': '-03-31',
  }
elif 'KY' in short_name:
    calendar = {
    'old':{
        '1': '-05-15',
        '2': '-08-14',
        '3': '-11-14',
        '4': '-03-31',
          },
    'new':{
        '1': '-05-15',
        '2': '-08-31',
        '3': '-11-14',
        '4': '-03-31',
          },
    }
else:
  calendar = {
    '1': '-05-15',
    '2': '-08-14',
    '3': '-11-14',
    '4': '-03-31',
  }
get_year = lambda year, season: str(year) if int(season) != 4 else str(int(year) + 1)
ky_policy_check = lambda year: 'new' if year >= '2021' else 'old'
return pd.to_datetime(s.index.map(lambda d: get_year(d[:4], d[-1]) + calendar[ky_policy_check(d[:4])][d[-1]]) if 'KY' in short_name else s.index.map(lambda d: get_year(d[:4], d[-1]) + calendar[d[-1]]))

def season_end(s):

calendar = {
  '1': '-3-31',
  '2': '-6-30',
  '3': '-9-30',
  '4': '-12-31',
}
return pd.to_datetime(s.index.map(lambda d: d[:4] + calendar[d[-1]]))

disclosure_dates = data.get('financial_statements_upload_detail:upload_date') disclosure_dates = disclosure_dates.apply(pd.to_datetime)

financial_season_end = disclosure_dates.apply(season_end) default_disclosure_dates = disclosure_dates.apply(calc_default_disclosure_dates)

disclosure_dates[(disclosure_dates > default_disclosure_dates) | (disclosure_dates < financial_season_end)] = pd.NaT disclosure_dates[(disclosure_dates.diff() <= datetime.timedelta(days=0))] = pd.NaT disclosure_dates.loc['2019-Q1', '3167'] = pd.NaT disclosure_dates.loc['2015-Q1', '5536'] = pd.NaT disclosure_dates.loc['2018-Q1', '5876'] = pd.NaT

disclosure_dates = disclosure_dates.fillna(default_disclosure_dates) disclosure_dates.columns.name = 'stock_id'

if detail: return disclosure_dates return default_disclosure_dates


## 本益成長比(月營收截止日換股)

https://doc.finlab.tw/tools/guide_for_beginners/

```python
from finlab import data
from finlab.backtest import sim
import finlab

finlab.login("cdnE+4n53DXjKkN8J7spHLvPq3xwycL6gfd0PaUL+UDWOAKroWHcXUNsN82ibihU#free")


rev = data.get('monthly_revenue:當月營收')
rev_ma3 = rev.average(3)
rev_ma12 = rev.average(12)

# 營收趨勢多頭策略
cond1 = rev_ma3/rev_ma12 > 1.1
cond2 = rev/rev.shift(1) > 0.9
cond_all = cond1 & cond2

pe = data.get('price_earning_ratio:本益比')
營業利益成長率 = data.get('fundamental_features:營業利益成長率')

# 本益成長比
peg = (pe/營業利益成長率)

# 本益成長比和原訊號相乘,若不持有則相乘後等於0
position = peg*(cond_all)

# 原訊號為0的不要選,若沒加這行且策略只選到7檔,之後還是會選3檔訊號為0(不持有)的補足10檔,執行這行就會排除訊號為0。
position = position[position>0]

# 選股挑本益成長比前10小的
position = position.is_smallest(10)

print(position)



# 月營收截止日換股
position = position.reindex(rev.index_str_to_date().index, method='ffill')
print(position)
input()
report = sim(position=position, name="策略教學範例:peg_rev", stop_loss=0.1, upload=False)
report.display()

股價淨值比

from finlab import data
from finlab.backtest import sim

pb = data.get('price_earning_ratio:股價淨值比')
close = data.get('price:收盤價')

position = (1/(pb * close) * (close > close.average(60)) * (close > 5)).is_largest(20)
report = sim(position, resample='Q',mae_mfe_window=30,mae_mfe_window_step=2)
report.display_mae_mfe_analysis()

is_smallest

import pandas as pd

def is_smallest(df, n):
    return df.astype(float).apply(lambda s: s.nsmallest(n), axis=1).reindex_like(df).notna()

df = pd.DataFrame({
    'A': [1.2, 2.5, 3.1],
    'B': [2.2, 1.8, 5.5],
    'C': [3.3, 4.5, 0.7],
    'D': [4.4, 5.5, 2.2],
    'E': [5.5, 3.3, 4.4]
})

print(df)
result = is_smallest(df, 2)
print(result)

#這個 DataFrame 有 5 列 3 行,包含 15 個浮點數值。現在需要按行找到每行最小的 2 個數值所在的列,將其對應的索引置為 True。
#
#使用以下程式碼進行處理:
#
#複製
#n = 2
#result = df.astype(float).apply(lambda s: s.nsmallest(n), axis=1).reindex_like(df).notna()
#這裡的 n 為 2,在上面的程式碼中,先將 df 轉換為浮點格式,然後按行進行遍歷,對每一行調用 s.nsmallest(n) 函數找到最小的 2 個數值所在的列,其它位置置為 NaN,返回一個 Series 對象。接著把這個 Series 對象與原始 DataFrame 進行重索引,並且補缺失值為 False,最終得到一個與原始 DataFrame 一樣大小的布林值 DataFrame。
#
#執行上述程式之後,可以得到以下結果:
#
#複製
#       A      B      C      D      E
#0  False   True  False  False  False
#1   True  False  False  False   True
#2  False   True   True  False  False
#這個結果的含義是:
#
#第一行最小的 2 個數值分別位於 B 列,因此 B 列為 True,其餘為 False;

夏普值

出處: https://www.finlab.tw/python%e6%96%b0%e6%89%8b%e6%95%99%e5%ad%b8%ef%bc%9a%e5%a4%8f%e6%99%ae%e6%8c%87%e6%95%b8%e7%ad%96%e7%95%a5/

import yfinance as yf
import pandas as pd
import numpy as np

def crawl_price(stock_id):
    stock = yf.Ticker(stock_id)
    df = stock.history(period="max")
    return df


twii = crawl_price("^TWII")
print(twii)
mean = twii['Close'].pct_change().rolling(252).mean()
std = twii['Close'].pct_change().rolling(252).std()

sharpe = mean / std

# sharpe ratio 平滑
sr = sharpe
srsma = sr.rolling(60).mean()

# sharpe ratio 的斜率
srsmadiff = srsma.diff()

# 計算買入賣出點
buy = (srsmadiff > 0) & (srsmadiff.shift() < 0)
sell = (srsmadiff < 0) & (srsmadiff.shift() > 0)

# 計算持有時間
hold = pd.Series(np.nan, index=buy.index)
hold[buy] = 1
hold[sell] = 0
hold.ffill(inplace=True)
hold.plot()

# 持有時候的績效
adj = twii['Close'][buy.index]
(adj.pct_change().shift(-1)+1).fillna(1)[hold == 1].cumprod().plot()


def backtest(a, b, c, d, plot=False):
    sr = sharpe
    srsma = sr.rolling(a).mean()

    srsmadiff = srsma.diff() * 100
    ub = srsmadiff.quantile(b)
    lb = srsmadiff.quantile(c)
    
    buy = ((srsmadiff.shift(d) < lb) & (srsmadiff > ub))
    sell = ((srsmadiff.shift(d) > ub) & (srsmadiff < lb))

    hold = pd.Series(np.nan, index=buy.index)
    hold[buy] = 1
    hold[sell] = 0

    hold.ffill(inplace=True)
    
    adj = twii['Close'][buy.index]

    if plot:
        (adj.pct_change().shift(-1)+1).fillna(1)[hold == 1].cumprod().plot()
        hold.plot()

    eq = (adj.pct_change().shift(-1)+1).fillna(1)[hold == 1].cumprod()
    if len(eq) > 0:
        return eq.iloc[-1]
    else:
        return 1


maxeq = 0
for a in range(100,200,20):
    for b in np.arange(0.3, 0.9, 0.03):
        for c in np.arange(0.3, 0.6, 0.03):
            for d in range(60, 180, 10):
                
                eq = backtest(a,b,c,d)
                
                if maxeq < eq:
                    maxeq = eq
                    print(eq, a,b,c,d)

Backtrader交易基礎

查看帳戶情況:

class TestStrategy(bt.Strategy):
    def next(self):
        print('當前可用資金', self.broker.getcash())
        print('當前總資產', self.broker.getvalue())
        print('當前持倉量', self.broker.getposition(self.data).size)
        print('當前持倉成本', self.broker.getposition(self.data).price)
        # 也可以直接獲取持倉
        print('當前持倉量', self.getposition(self.data).size)
        print('當前持倉成本', self.getposition(self.data).price)
        # 註:getposition() 需要指定具體的標的資料集

滑點設定:

# 方式1:通過 BackBroker 類中的 slip_perc 參數設定百分比滑點
cerebro.broker = bt.brokers.BackBroker(slip_perc=0.0001)
# 方式2:通過呼叫 brokers 的 set_slippage_perc 方法設定百分比滑點
cerebro.broker.set_slippage_perc(perc=0.0001)

# 方式1:通過 BackBroker 類中的 slip_fixed 參數設定固定滑點
cerebro.broker = bt.brokers.BackBroker(slip_fixed=0.001)
# 方式2:通過呼叫 brokers 的 set_slippage_fixed 方法設定固定滑點
cerebro.broker = cerebro.broker.set_slippage_fixed(fixed=0.001)

參數說明:

有關滑點的其他設定 slip_open:是否對開盤價做滑點處理,該參數在 BackBroker() 類中默認為 False,在 set_slippage_perc 和set_slippage_fixed 方法中默認為 True; slip_match:是否將滑點處理後的新成交價與成交當天的價格區間 low ~ high 做匹配,如果為 True,則根據新成交價重新匹配調整價格區間,確保訂單能被執行;如果為 False,則不會與價格區間做匹配,訂單不會執行,但會在下一日執行一個空訂單;默認取值為 True; slip_out:如果新成交價高於最高價或低於最高價,是否以超出的價格成交,如果為 True,則允許以超出的價格成交;如果為 False,實際成交價將被限制在價格區間內 low ~ high;默認取值為 False; slip_limit:是否對限價單執行滑點,如果為 True,即使 slip_match 為Fasle,也會對價格做匹配,確保訂單被執行;如果為 False,則不做價格匹配;默認取值為 True。

# 情況1:
set_slippage_fixed(fixed=0.35,
                   slip_open=False,
                   slip_match=True,
                   slip_out=False)
# 由於 slip_open=False ,不會對開盤價做滑點處理,所以仍然以原始開盤價 32.63307367 成交

# 情況2:
set_slippage_fixed(fixed=0.35,
                   slip_open=True,
                   slip_match=True,
                   slip_out=False)

# 情況3:
set_slippage_fixed(fixed=0.35,
                   slip_open=True,
                   slip_match=True,
                   slip_out=True)
# 滑點調整的新成交價為 32.63307367+0.35 = 32.98307367,超出了當天最高價 32.94151482
# 允許做價格匹配 slip_match=True, 而且運行以超出價格區間的新成交價執行 slip_out=True
# 最終以新成交價 32.98307367 成交

# 情況4:
set_slippage_fixed(fixed=0.35,
                   slip_open=True,
                   slip_match=False,
                   slip_out=True)
# 滑點調整的新成交價為 32.63307367+0.35 = 32.98307367,超出了當天最高價 32.94151482
# 由於不進行價格匹配 slip_match=False,新成交價超出價格區間無法成交
# 2019-01-17 這一天訂單不會執行,但會在下一日 2019-01-18 執行一個空訂單
# 再往後的 2019-07-02,也未執行訂單,下一日 2019-07-03 執行空訂單
# 即使 2019-07-03的 open 39.96627412+0.35 < high 42.0866713 滿足成交條件,也不會補充成交

交易稅費管理

股票:目前 A 股的交易費用分為 2 部分:佣金和印花稅, 其中佣金雙邊徵收,不同證券公司收取的佣金各不相同,一般在 0.02%-0.03% 左右,單筆佣金不少於 5 元; 印花稅只在賣出時收取,稅率為 0.1%。

期貨:期貨交易費用包括交易所收取手續費和期貨公司收取佣金 2 部分,交易所手續費較為固定, 不同期貨公司佣金不一致,而且不同期貨品種的收取方式不相同,有的按照固定費用收取,有的按成交金額的固定百分比收取: 合約現價合約乘數手續費費率。除了交易費用外,期貨交易時還需上交一定比例的保證金 。

Backtrader 也提供了多種交易費設定方式,既可以簡單的通過參數進行設定,也可以結合交易條件自訂費用函數:

根據交易品種的不同,Backtrader 將交易費用分為 股票 Stock-like 模式和期貨 Futures-like 種模式; 根據計算方式的不同,Backtrader 將交易費用分為 PERC 百分比費用模式 和 FIXED 固定費用模式 ;

Stock-like 模式與 PERC 百分比費用模式對應,期貨 Futures-like 與 FIXED 固定費用模式對應;

在設定交易費用時,最常涉及如下 3 個參數:

commission:手續費 / 佣金;

mult:乘數;

margin:保證金 / 保證金比率 。

雙邊徵收:買入和賣出操作都要收取相同的交易費用 。

cerebro.broker.setcommission(
    # 交易手續費,根據margin取值情況區分是百分比手續費還是固定手續費
    commission=0.0,
    # 期貨保證金,決定著交易費用的類型,只有在stocklike=False時起作用
    margin=None,
    # 乘數,盈虧會按該乘數進行放大
    mult=1.0,
    # 交易費用計算方式,取值有:
    # 1.CommInfoBase.COMM_PERC 百分比費用
    # 2.CommInfoBase.COMM_FIXED 固定費用
    # 3.None 根據 margin 取值來確定類型
    commtype=None,
    # 當交易費用處於百分比模式下時,commission 是否為 % 形式
    # True,表示不以 % 為單位,0.XX 形式;False,表示以 % 為單位,XX% 形式
    percabs=True,
    # 是否為股票模式,該模式通常由margin和commtype參數決定
    # margin=None或COMM_PERC模式時,就會stocklike=True,對應股票手續費;
    # margin設定了取值或COMM_FIXED模式時,就會stocklike=False,對應期貨手續費
    stocklike=False,
    # 計算持有的空頭頭寸的年化利息
    # days * price * abs(size) * (interest / 365)
    interest=0.0,
    # 計算持有的多頭頭寸的年化利息
    interest_long=False,
    # 槓桿比率,交易時按該槓桿調整所需現金
    leverage=1.0,
    # 自動計算保證金
    # 如果False,則通過margin參數確定保證金
    # 如果automargin<0,通過mult*price確定保證金
    # 如果automargin>0,如果automargin*price確定保證金
    automargin=False,
    # 交易費用設定作用的資料集(也就是作用的標的)
    # 如果取值為None,則默認作用於所有資料集(也就是作用於所有assets)
    name=None)

從上述各參數的含義和作用可知,margin 、commtype、stocklike 存在 2 種默認的組態規則:股票百分比費用、期貨固定費用,具體如下: 第 1 條規則:未設定 margin(即 margin 為 0 / None / False)→ commtype 會指向 COMM_PERC 百分比費用 → 底層的 _stocklike 屬性會設定為 True → 對應的是“股票百分比費用”。 所以如果想為股票設定交易費用,就令 margin = 0 / None / False,或者令 stocklike=True;

第 2 條規則:為 margin 設定了取值 → commtype 會指向 COMM_FIXED 固定費用 → 底層的 _stocklike 屬性會設定為 False → 對應的是“期貨固定費用”,因為只有期貨才會涉及保證金。 所以如果想為期貨設定交易費用,就需要設定 margin,此外還需令 stocklike=True,margin 參數才會起作用 。

自訂交易費用的例子

# 自訂期貨百分比費用
class CommInfo_Fut_Perc_Mult(bt.CommInfoBase):
    params = (
      ('stocklike', False), # 指定為期貨模式
      ('commtype', bt.CommInfoBase.COMM_PERC), # 使用百分比費用
      ('percabs', False), # commission 以 % 為單位
    )

    def _getcommission(self, size, price, pseudoexec):
        # 計算交易費用
        return (abs(size) * price) * (self.p.commission/100) * self.p.mult
    # pseudoexec 用於提示當前是否在真實統計交易費用
    # 如果只是試算費用,pseudoexec=False
    # 如果是真實的統計費用,pseudoexec=True

comminfo = CommInfo_Fut_Perc_Mult(
    commission=0.1, # 0.1%
    mult=10,
    margin=2000) # 實例化
cerebro.broker.addcommissioninfo(comminfo)

# 上述自訂函數,也可以通過 setcommission 來實現
cerebro.broker.setcommission(commission=0.1, #0.1%
                             mult=10,
                             margin=2000,
                             percabs=False,
                             commtype=bt.CommInfoBase.COMM_PERC,
                             stocklike=False)

下面是考慮佣金和印花稅的股票百分比費用:

class StockCommission(bt.CommInfoBase):
    params = (
      ('stocklike', True), # 指定為期貨模式
      ('commtype', bt.CommInfoBase.COMM_PERC), # 使用百分比費用模式
      ('percabs', True), # commission 不以 % 為單位
      ('stamp_duty', 0.001),) # 印花稅默認為 0.1%
    
    # 自訂費用計算公式
      def _getcommission(self, size, price, pseudoexec):
            if size > 0: # 買入時,只考慮佣金
                return abs(size) * price * self.p.commission
            elif size < 0: # 賣出時,同時考慮佣金和印花稅
        return abs(size) * price * (self.p.commission + self.p.stamp_duty)
            else:
                return 0

成交量限制管理

形式1:bt.broker.fillers.FixedSize(size)

通過 FixedSize() 方法設定最大的固定成交量:size,該種模式下的成交量限制規則如下:

訂單實際成交量的確定規則:取(size、訂單執行那天的 volume 、訂單中要求的成交數量)中的最小者;

訂單執行那天,如果訂單中要求的成交數量無法全部滿足,則只成交部分數量。第二天不會補單。

# 通過 BackBroker() 類直接設定
cerebro = Cerebro()
filler = bt.broker.fillers.FixedSize(size=xxx)
newbroker = bt.broker.BrokerBack(filler=filler)
cerebro.broker = newbroker

# 通過 set_filler 方法設定
cerebro = Cerebro()
cerebro.broker.set_filler(bt.broker.fillers.FixedSize(size=xxx))

# self.order = self.buy(size=2000) # 每次買入 2000 股
# cerebro.broker.set_filler(bt.broker.fillers.FixedSize(size=3000)) # 固定最大成交量

形式2:bt.broker.fillers.FixedBarPerc(perc)

通過 FixedBarPerc(perc) 將 訂單執行當天 bar 的總成交量 volume 的 perc % 設定為最大的固定成交量,該模式的成交量限制規則如下:

訂單實際成交量的確定規則:取 (volume * perc /100、訂單中要求的成交數量) 的 最小者; 訂單執行那天,如果訂單中要求的成交數量無法全部滿足,則只成交部分數量。

# 通過 BackBroker() 類直接設定
cerebro = Cerebro()
filler = bt.broker.fillers.FixedBarPerc(perc=xxx)
newbroker = bt.broker.BrokerBack(filler=filler)
cerebro.broker = newbroker

# 通過 set_filler 方法設定
cerebro = Cerebro()
cerebro.broker.set_filler(bt.broker.fillers.FixedBarPerc(perc=xxx))
# perc 以 % 為單位,取值範圍為[0.0,100.0]

# self.order = self.buy(size=2000) # 以下一日開盤價買入2000股
# cerebro.broker.set_filler(bt.broker.fillers.FixedBarPerc(perc=50))

形式3:bt.broker.fillers.BarPointPerc(minmov=0.01,perc=100.0)

BarPointPerc() 在考慮了價格區間的基礎上確定成交量,在訂單執行當天,成交量確定規則為:

通過 minmov 將 當天 bar 的價格區間 low ~ high 進行均勻劃分,得到劃分的份數:

part = (high -low +minmov) // minmov (向下取整)

再對當天 bar 的總成交量 volume 也劃分成相同的份數 part ,這樣就能得到每份的平均成交量:

volume_per = volume // part

最終,volume_per * (perc / 100)就是允許的最大成交量,實際成交時,對比訂單中要求的成交量,就可以得到最終實際成交量

實際成交量 = min ( volume_per * (perc / 100), 訂單中要求的成交數量 )

# 通過 BackBroker() 類直接設定
cerebro = Cerebro()
filler = bt.broker.fillers.BarPointPerc(minmov=0.01,perc=100.0)
newbroker = bt.broker.BrokerBack(filler=filler)
cerebro.broker = newbroker

# 通過 set_filler 方法設定
cerebro = Cerebro()
cerebro.broker.set_filler(bt.broker.fillers.BarPointPerc(minmov=0.01,perc=100.0))
# perc 以 % 為單位,取值範圍為[0.0,100.0]


# self.order = self.buy(size=2000) # 以下一日開盤價買入2000股

# cerebro.broker.set_filler(bt.broker.fillers.BarPointPerc(minmov=0.1, perc=50)) # 表示 50%

交易時機管理 對於交易訂單生成和執行時間,Backtrader 默認是 “當日收盤後下單,次日以開盤價成交”,這種模式在回測過程中能有效避免使用未來資料。 但對於一些特殊的交易場景,比如“all_in”情況下,當日所下訂單中的數量是用當日收盤價計算的(總資金 / 當日收盤價),次日以開盤價執行訂單時, 如果開盤價比昨天的收盤價提高了,就會出現可用資金不足的情況。 為了應對一些特殊交易場景,Backtrader 還提供了一些 cheating 式的交易時機模式:Cheat-On-Open 和 Cheat-On-Close。

Cheat-On-Open

Cheat-On-Open 是“當日下單,當日以開盤價成交”模式,在該模式下,Strategy 中的交易邏輯不再寫在 next() 方法裡,而是寫在特定的 next_open()、nextstart_open() 、prenext_open() 函數中,具體設定可參考如下案例:

方式1:bt.Cerebro(cheat_on_open=True); 方式2:cerebro.broker.set_coo(True); 方式3:BackBroker(coo=True)。

Cheat-On-Close

Cheat-On-Close 是“當日下單,當日以收盤價成交”模式,在該模式下,Strategy 中的交易邏輯仍寫在 next() 中,具體設定如下:

方式1:cerebro.broker.set_coc(True); 方式2:BackBroker(coc=True)

class TestStrategy(bt.Strategy):
    ......
    def next(self):
        # 取消之前未執行的訂單
        if self.order:
            self.cancel(self.order)
        # 檢查是否有持倉
        if not self.position:
            # 10日均線上穿5日均線,買入
            if self.crossover > 0:
                print('{} Send Buy, open {}'.format(self.data.datetime.date(),self.data.open[0]))
                self.order = self.buy(size=100) # 以下一日開盤價買入100股
        # # 10日均線下穿5日均線,賣出
        elif self.crossover < 0:
            self.order = self.close() # 平倉,以下一日開盤價賣出
    ......

# 實例化大腦
cerebro= bt.Cerebro()
.......
# 當日下單,當日收盤價成交
cerebro.broker.set_coc(True)

Python回測框架(一)Backtrader 介紹

為什麼設計投資策略需要「回測」?

出處: https://stockbuzzai.wordpress.com/2019/07/08/python%e5%9b%9e%e6%b8%ac%e6%a1%86%e6%9e%b6%ef%bc%88%e4%b8%80%ef%bc%89backtrader-%e4%bb%8b%e7%b4%b9/

因為人都有盲點,在股市暗潮洶湧的環境中,會有我們沒考量到的變因潛伏期中。透過回測,我們可以找到之前沒發現的暗礁使投資策略更完備、更安全航行於股海之中。

在這邊介紹一個能簡單操作回測的程式 Backtrader,讓你能輕鬆上手,用電腦幫你運用過去的數據測試選股策略!

Backtrader 介紹

Backtrader 是一套基於 Python 的策略回測框架,可以讓使用者花更多時間專注於策略上而不需要處理一些交易細節和輸出回測的結果。

這是一個簡單的範例。內容很簡單,我們先放入一筆錢,然後每月買一張微軟 (MSFT) 的股票,最後輸出回測的結果。

data = bt.feeds.YahooFinanceData(dataname='MSFT',
                                  fromdate=datetime(2010, 1, 1),
                                  todate=datetime(2018, 12, 31))
  
cerebro.adddata(data) 

在回測中最重要的,就是需要有數據,Backtrader 提供了從 Yahoo!Finance 直接撈取資料的功能,在這一段程式碼中,我們下載 MSFT 從 2010/1/1 到 2018/12/31 的股價資料,並把資料添加到 cerebro。

Backtrader 除了支援使用 Yahoo!Finance 撈取資料之外,也支援從 csv 載入資料,有興趣的人可以查看 Data Feeds

class TestStrategy(bt.Strategy):
     def __init__(self):
         self._next_buy_date = datetime(2010, 1, 5)
  
     def next(self):
         if self.data.datetime.date() >= self._last_buy_date.date():
             self._last_buy_date += relativedelta(months=1)
             self.buy(size=1)  

處理完資料的問題,接下來就可以專注於策略的部分。我們的策略是每月 5 日購買 1 股股票。

def __init__(self):
         self._next_buy_date = datetime(2010, 1, 5)

我們的資料設定的時間是 2010/1/1 到 2018/12/31,因此我們設定第一個購買股票的日期為 2010/1/5。

def next(self):
         if self.data.datetime.date() >= self._last_buy_date.date():
             self._last_buy_date += relativedelta(months=1)
             self.buy(size=1)  

next 這個 method 是每一個交易日都會被呼叫一次,我們在這個 method 中比對交易日是否已經到達我們的預期的交易日,當到達的時候就把交易日加上一個月,並購買一股。

cerebro.adddata(data) 
cerebro.addstrategy(TestStrategy)
cerebro.broker.set_cash(cash=10000)
cerebro.run()
cerebro.plot()

我們將策略添加到 cerebro 之中,同時不要忘了把我們的起始資金設定為 10000,然後呼叫 cerebro.run() 來直接交易模擬的工作。最後,我們希望把結果用圖型化的方式輸出,於是呼叫 cerebro.plot() 來展示結果。

以下是我們輸出的結果:

img

由圖可見, 最上方藍線為淨值曲線,紅線為現金值曲線;最下方為 MSFT 價格走勢圖和我們的買入點,我們的帳戶淨值由原始的 10,000 增長至 16,021.77。在這段期間,我們總共買入 108 股,買入的平均價格為 43.83 元,資產多了 6021.77 元!

下次,我們來回測定期定額買入股票的績效。

__

完整程式碼:

from datetime import datetime
import backtrader as bt
from dateutil.relativedelta import relativedelta
  
class TestStrategy(bt.Strategy):
     def __init__(self):
         self._next_buy_date = datetime(2010, 1, 5)
  
     def next(self):
         if self.data.datetime.date() >= self. _next_buy_date.date():
             self. _next_buy_date += relativedelta(months=1)
             self.buy(size=1)
  
cerebro = bt.Cerebro()
data = bt.feeds.YahooFinanceData(dataname='MSFT',
                                  fromdate=datetime(2010, 1, 1),
                                  todate=datetime(2018, 12, 31))
  
cerebro.adddata(data) 
cerebro.addstrategy(TestStrategy)
cerebro.broker.set_cash(cash=10000)
cerebro.run()
cerebro.plot() 

Python 回測框架(二)定期定額投資

出處: https://stockbuzzai.wordpress.com/2019/07/09/python-%e5%9b%9e%e6%b8%ac%e6%a1%86%e6%9e%b6-%e4%ba%8c%ef%bc%9a%e5%ae%9a%e6%9c%9f%e5%ae%9a%e9%a1%8d%e6%8a%95%e8%b3%87/

在上一篇 Python回測框架 (一): Backtrader介紹 中我們介紹如何使用 Backtrader 做定期投資,可是一般我們投資不會一開始準備一大筆錢,然後每期讓它扣款,通常是定期定額的投資。因此在這篇文章中會介紹如何使用 Backtrader 做定期定額的投資。

def __init__(self):
         self._last_deposit_date = datetime(2010, 1, 5)
         self._last_buy_date = datetime(2010, 1, 7)

在初始化的部分,我們一共分成兩個變數,_last_deposit_date 是紀錄本月的入金時間,_last_buy_date 則是記錄實際購買的時間。因為 Backtrader 在增加現金至系統的時候,我有兩天的延遲,所以必須在入金的兩天之後才能進行股票的購買。

def next(self):
         current_date = self.data.datetime.date()
         if current_date >= self._last_deposit_date.date():
             self._last_deposit_date += relativedelta(months=1)
             self._last_buy_date = datetime.combine(date=current_date, time=datetime.min.time()) + relativedelta(days=3)
             self.broker.add_cash(cash=300)

在處理每個交易日的部分,我們程式碼一共分成兩段,這一段程式碼是先判斷今天是不是匯款日,如果是匯款日的話,將 300 美金存入銀行,同時將 _last_deposit_date 設定為下一個月的同一天,並且把交易日設定為三天後。

if current_date >= self._last_buy_date.date():
             price = (self.data.high + self.data.low) / 2.0
             volume = math.floor((self.broker.cash) / price)
             self.buy(size=volume)
             self._last_buy_date += relativedelta(months=1)

這一段程式碼是判斷當今天為交易日的時候,我們使用當日最高價(self.data.high)和最低價(self.data.low)的平均來計算要買的量。把持有的現金(self.broker.cash)除上價格就是我們預計買的量。同時把交易日加上一個月。

cerebro.broker.set_cash(cash=1)

最後,因為我們這次是採取定期定額的投資,所以我們把錢設定為 1 (因為設定為 0 的時候在資產計算會 Crash)。

以下分別是每月用 300 美元購買微軟 (MSFT)、好市多 (COST)、星巴克 (SBUX) 的狀況,購買的時間是 2010/01/01 到 2018/12/31。其中明顯的可以發現微軟的總淨值 94755.7 比好市多的 69478.5 和星巴克的 77851.72 還要好 10% 以上,因此慎選好股票是很重要的一件事情。

img微軟 (MSFT)

img好市多 (COST)

img星巴克 (SBUX)

___

完整程式碼:

from datetime import datetime
from dateutil.relativedelta import relativedelta
import backtrader
import math
  
class TestStrategy(backtrader.Strategy):
     def __init__(self):
         self._last_deposit_date = datetime(2010, 1, 5)
         self._last_buy_date = datetime(2010, 1, 7)
  
     def next(self):
         current_date = self.data.datetime.date()
         if current_date >= self._last_deposit_date.date():
             self._last_deposit_date += relativedelta(months=1)
             self._last_buy_date = datetime.combine(date=current_date, time=datetime.min.time()) + relativedelta(days=3)
             self.broker.add_cash(cash=300)
  
         if current_date >= self._last_buy_date.date():
             price = (self.data.high + self.data.low) / 2.0
             volume = math.floor((self.broker.cash) / price)
             self.buy(size=volume)
             self._last_buy_date += relativedelta(months=1)
  
cerebro = backtrader.Cerebro()
data = backtrader.feeds.YahooFinanceData(dataname='MSFT',
                                  fromdate=datetime(2010, 1, 1),
                                  todate=datetime(2018, 12, 31))
  
cerebro.adddata(data) 
cerebro.addstrategy(TestStrategy)
cerebro.broker.set_cash(cash=1)
cerebro.run()
cerebro.plot() 

Python 回測框架(三)技術指標

出處:https://stockbuzzai.wordpress.com/2019/07/10/python-%e5%9b%9e%e6%b8%ac%e6%a1%86%e6%9e%b6%ef%bc%88%e4%b8%89%ef%bc%89%e6%8a%80%e8%a1%93%e6%8c%87%e6%a8%99/

延續之前的內容,這次要利用最頻繁被使用的技術指標之一 – 日均線 (Moving Average) 作為我們策略的篩選條件。

在投資策略上,技術指標是一個很常被用到的工具。透過技術指標,我們可以比較容易地分析商品或是大盤的趨勢。在這個範例之中,我們將使用 20 日均線和 60 日均線做為我們買賣的參考。當商品 (MSFT) 的開盤價低於 60 日均線同時 20 均線又呈現上漲趨勢的時候,我們才買入商品。

data = backtrader.feeds.YahooFinanceData(dataname='MSFT',
                                  fromdate=datetime(2009, 1, 1),
                                  todate=datetime(2018, 12, 31)) 

雖然我們是從 2010 年才開始交易,但是因為我們有需要 60 天均線和 20 天均線的資料,因此我們必須至少多載入 60 個交易日以上的資料來提供系統計算均線。

def __init__(self):
         self.next_buy_date = datetime(2010, 1, 1)
         self.sma60 = backtrader.ind.SMA(period=60)
         self.sma20 = backtrader.ind.SMA(period=20)
         self.total_cash = 0 

在初始化的過程中,我們產生了兩個均線變數,sma20 和 sma60。同時我們產生一個變數 total_cash 來統計我們到底投入的多少資金。next_buy_day則是控制不要短期內連續買入。

current_date = self.data.datetime.date()
         if current_date >= self.next_buy_date.date():
             if self.data.close < self.sma60[0] and self.sma20[0] > self.sma20[-1]:
                 price = (self.data.high + self.data.low) / 2.0
                 volume = math.floor((self.broker.cash) / price)
                 self.buy(size=volume)
                 self.broker.add_cash(cash=300)
                 self.total_cash += 300
                 self.next_buy_date = datetime.combine(current_date, datetime.min.time()) + relativedelta(months=1)  

首先,if current_date >= self.next_buy_date.date(): 這一行判斷了是否今天是可以交易的日子。self.data.close < self.sma60[0] 則判斷了今日的開盤價是否高於 60 日均線。self.sma20[0] > self.sma20[-1] 這一行則判斷今日的 20 日均線值是否比昨天高,用來判斷20 天均線是否在上升的狀態。

當這兩個條件都成立,則買入。

根據程式回測的結果,我們只需要投入 7800 元,9 年後微軟的淨值就會成為 25474,大約賺了250% 以上。

img微軟 (MSFT)

___

完整程式碼

from datetime import datetime
from dateutil.relativedelta import relativedelta
import backtrader
import math
  
class TestStrategy(backtrader.Strategy):
     def __init__(self):
         self.next_buy_date = datetime(2010, 1, 1)
         self.sma60 = backtrader.ind.SMA(period=60)
         self.sma20 = backtrader.ind.SMA(period=20)
         self.total_cash = 0
  
     def next(self):
         current_date = self.data.datetime.date()
         if current_date >= self.next_buy_date.date():
             if self.data.close < self.sma60[0] and self.sma20[0] > self.sma20[-1]:
                 price = (self.data.high + self.data.low) / 2.0
                 volume = math.floor((self.broker.cash) / price)
                 self.buy(size=volume)
                 self.broker.add_cash(cash=300)
                 self.total_cash += 300
                 self.next_buy_date = datetime.combine(current_date, datetime.min.time()) + relativedelta(months=1)
  
     def stop(self):
         print(self.total_cash)
  
 cerebro = backtrader.Cerebro()
 data = backtrader.feeds.YahooFinanceData(dataname='MSFT',
                                  fromdate=datetime(2009, 1, 1),
                                  todate=datetime(2018, 12, 31))
  
 cerebro.adddata(data) 
 cerebro.addstrategy(TestStrategy)
 cerebro.broker.set_cash(cash=300)
 cerebro.run()
 cerebro.plot() 

Python 回測框架(四)CrossOver 和 Signal

出處:https://stockbuzzai.wordpress.com/2019/07/15/python-%e5%9b%9e%e6%b8%ac%e6%a1%86%e6%9e%b6%ef%bc%88%e5%9b%9b%ef%bc%89crossover-%e5%92%8c-signal/

CrossOver

什麼是 CrossOver 呢?CrossOver 是一個判斷輸入的價格或是技術指標狀態的工具。

首先我們先來看下面這一段程式碼:

def __init__(self):
     sma10 = backtrader.ind.SMA(period=10)
     sma30 = backtrader.ind.SMA(period=30)
     self.crossover = backtrader.ind.CrossOver(sma10, sma30)  

在這段程式碼之中,我們建立了一條 10 日的短均線 (sma10) 和一條 30 日的長均線 (sma30)。接著,我們利用了 sma10 和 sma30 建立了一個 CrossOver。

當短均線向上穿越長均線時,sma10 的數值變得比 sma30 大,此時 CrossOver 會回傳一個大於 0 的數值,做為買進訊號。反之,當短均線向下穿越長均線時,sma10 的數值變得小於 sma30 ,CrossOver 會回傳一個小於 0 的數值,為賣出訊號。

以下我們利用 CrossOver 來做買賣:

def next(self):
     if not self.position:
         if self.crossover > 0:
             self.buy()
  
     elif self.crossover < 0:
         self.close() 

這段程式碼中,當我們沒有持倉,且 crossover 大於 0,我們買進股票 1 股 (一樣是以微軟股票為例),當 crossover 小於 0,我們平倉。

CROSSOVER 完整程式碼

from datetime import datetime
import backtrader
  
  
class SmaCross(backtrader.Strategy):
     def __init__(self):
         sma10 = backtrader.ind.SMA(period=10)
         sma30 = backtrader.ind.SMA(period=30)
         self.crossover = backtrader.ind.CrossOver(sma10, sma30)
  
     def next(self):
         if not self.position:
             if self.crossover > 0:
                 self.buy()
  
         elif self.crossover < 0:
             self.close()
  
  
cerebro = backtrader.Cerebro()
data = backtrader.feeds.YahooFinanceData(dataname='MSFT',
                                          fromdate=datetime(2011, 1, 1),
                                          todate=datetime(2012, 12, 31))
  
cerebro.adddata(data)
cerebro.addstrategy(SmaCross)
cerebro.run()
cerebro.plot() 

根據這個程式碼,以下是我們的回測結果。我們可以清楚地看當 sma10 和 sma30 發生交叉之後,我們就買進和賣出對應的股票。

img微軟 (MSFT)

Signal

很多人可能覺得 CrossOver 可能沒什麼用處,因為我們直接比較 sma10 和 sma30 的數值也可以得到相同的結果,不需要用到這個工具。其實這個工具的主要用途是配合我們現在要介紹的另外一個工具 Signal 來使用。

以下我們先來看一段程式碼:

class SmaCross(backtrader.SignalStrategy):
     def __init__(self):
         sma10 = backtrader.ind.SMA(period=10)
         sma30 = backtrader.ind.SMA(period=30)
         crossover = backtrader.ind.CrossOver(sma10, sma30)
         self.signal_add(backtrader.SIGNAL_LONG, crossover) 

我們把 SmaCross 這個策略跟之前的比較,我們把 Strategy 換成了 SignalStrategy。在初始化的部分,我們的前三步驟跟之前一樣,分別建立了一個 10 日、30 日均線和一個 CrossOver,最大的不同是我們把這個 CrossOver 添加進了一個 Signal 之中。

Signal 會根據傳入資料的狀態變化時來進行動作。當傳入資料的變成正的,則發出 long 的信號,當傳入資料變為負的,則發出 short 的信號。當傳入資料變為 0,則不發出信號。至於 long 和 short 的行為則根據第一個參數來定義。目前這個參數一共有五種選擇分別是:

MAIN GROUP:

  • LONGSHORT:long 和 short 的信號都接收

  • LONG:

    • 收到 long 的信號時會買入

    • 收到 short 的信號時會平倉,但是

      • 如果系統中有 LONGEXIT 的信號,會先離場

      • 如果系統中有 SHORT 的信號而且沒有 LONGEXIT 的信號,會先離場再賣出。

  • SHORT:

    • 收到 short 的信號時會賣出

    • 收到 long 的信號時會平倉,但是

      • 如果系統中有 SHORTEXIT 的信號,會先離場。

      • 如果有 LONG 的信號而且沒有 SHORTEXIT 的信號,會先離場再買入。

EXIT GROUP:

  • LONGEXIT:收到 short 會離場
  • SHORTEXIT:收到 long 會離場

當使用 LONG 的時候,我們可以看到跟之前相同的的結果:

imgLONG

如果將 LONG 改為 LONGSHORT,我們將得到另外一個結果:

imgLONGSHORT

完整的程式碼

from datetime import datetime
import backtrader
  
  
class SmaCross(backtrader.SignalStrategy):
     def __init__(self):
         sma10 = backtrader.ind.SMA(period=10)
         sma30 = backtrader.ind.SMA(period=30)
         crossover = backtrader.ind.CrossOver(sma10, sma30)
         self.signal_add(backtrader.SIGNAL_LONG, crossover)
  
  
cerebro = backtrader.Cerebro()
data = backtrader.feeds.YahooFinanceData(dataname='MSFT',
                                          fromdate=datetime(2011, 1, 1),
                                          todate=datetime(2012, 12, 31))
  
cerebro.adddata(data)
cerebro.addstrategy(SmaCross)
cerebro.run()
cerebro.plot() 

Python 回測框架(五)Sizer

出處:https://stockbuzzai.wordpress.com/2019/07/23/python-%e5%9b%9e%e6%b8%ac%e6%a1%86%e6%9e%b6%ef%bc%88%e4%ba%94%ef%bc%89sizer/

在上一篇 Python 回測框架(四)CrossOver 和 Signal 中,我們談到了如何使用CrossOver 和 Signal 來買賣商品。但是我們又遇到了一個問題,我們無法控制買賣的商品數量。在一般交易中,我們可能會根據當前的狀況來買賣不同的數量的商品,因此我們在這一篇文章要介紹的就是 Backtrader 中負責控制買賣商品數量的工具 Sizer。

首先我們先回看下面這段程式碼:

from datetime import datetime
import backtrader
  
class SmaCross(backtrader.SignalStrategy):
    def __init__(self):
        sma10 = backtrader.ind.SMA(period=10)
        sma30 = backtrader.ind.SMA(period=30)
        crossover = backtrader.ind.CrossOver(sma10, sma30)
        self.signal_add(backtrader.SIGNAL_LONG, crossover)
  
cerebro = backtrader.Cerebro()
data = backtrader.feeds.YahooFinanceData(dataname='MSFT',
                                         fromdate=datetime(2011, 1, 1),
                                         todate=datetime(2012, 12, 31))
  
cerebro.adddata(data)
cerebro.addstrategy(SmaCross)
cerebro.addsizer(backtrader.sizers.SizerFix, stake=10)
cerebro.run()
cerebro.plot() 

這一段程式碼幾乎跟 Python 回測框架(四)CrossOver 和 Signal 中的程式碼一樣,但是我們多了一行程式碼:

cerebro.addsizer(backtrader.sizers.SizerFix, stake=10) 

這裡我們設定了一個 Sizer,這個 Sizer 每次買賣固定的數量,一次是 10 股。下列是設定不同的 stake 的值的時候所產生的結果。

imgstake = 1

stake=1 的時候,產生的結果跟 Python 回測框架(四)CrossOver 和 Signal 中是相同的。

imgstake = 10

stake=10 的時候,我們發現每次購買股票資金明顯花費的比較多。

imgstake = 100

stake=100 的時候,我們發現每次購買的的數量確實更多了,所花費的資金也更多。

另外,在 Backtrader 之中,主要分成兩種 sizer。一種 sizer 屬於預設的 sizer,在系統中如果沒有其他的 sizer,就使用這個 sizer。在之前的程式碼就是使用預設 sizer 的範例。

另一種 sizer 是跟隨著 strategy 的,這種 sizer 只處理對應的 strategy 的買賣。設定strategy 的 sizer 有二種方法,一種是透過 cerebro 的 addsizer_byidx 來設定,如以下範例:

idx = cerebro.addstrategy(SmaCross)
cerebro.addsizer_byidx(idx, backtrader.sizers.SizerFix, stake=100) 

其中 idx 是 cerebro 在新增 strategy 的時候所回傳 idx 值,以此當參數來新增 sizer。

第二種方式是透過 strategy 的 setsizer 來設定 sizer,程式碼如下:

self.setsizer(backtrader.sizers.SizerFix(stake=100))

如何自訂 sizer

要撰寫自訂的 sizer,需要繼承 backtrader.Sizer 這個物件,然後覆寫 _getsizing 這個函式,以下是範例:

class AllSizer(backtrader.Sizer):
    def _getsizing(self, comminfo, cash, data, isbuy):
        if isbuy:
            return math.floor(cash/data.high)
        else:
            return self.broker.getposition(data) 

AllSizer 是一個直接將資金買入商品的 sizer,其中 comminfo 代表手續費的相關資訊,cash 代表目前持有的現金,data 代表目前這檔商品,isbuy 代表這是買入還是賣出的請求。在買入的部分,我們利用持有現金除上當天最高價計算買入的股票數。賣出的時候則是把所有持倉都賣出。

以下是完整的程式碼和執行的結果:

from datetime import datetime
import yfinance as yf
import backtrader
import math


class AllSizer(backtrader.Sizer):
    def _getsizing(self, comminfo, cash, data, isbuy):
        if isbuy:
            return math.floor(cash / data.high)
        else:
            return self.broker.getposition(data)


class SmaCross(backtrader.SignalStrategy):
    def __init__(self):
        sma10 = backtrader.ind.SMA(period=10)
        sma30 = backtrader.ind.SMA(period=30)
        crossover = backtrader.ind.CrossOver(sma10, sma30)
        self.signal_add(backtrader.SIGNAL_LONG, crossover)

        self.setsizer(AllSizer())


cerebro = backtrader.Cerebro()
# data = backtrader.feeds.PandasData(dataname=yf.download('TSLA', '2018-01-01', '2023-01-01'))
data = backtrader.feeds.PandasData(
    dataname=yf.download("MSFT", "2011-01-01", "2023-01-01")
)

# data = backtrader.feeds.YahooFinanceData(
#    dataname="MSFT", fromdate=datetime(2011, 1, 1), todate=datetime(2012, 12, 31)
# )

cerebro.adddata(data)
idx = cerebro.addstrategy(SmaCross)
cerebro.run()
# pip install matplotlib==3.2.2
cerebro.plot()

img

有了 sizer,我們就可以把計算買賣商品數量的邏輯獨立,讓我們在撰寫回測程式的時候更方便且更有彈性。

Backtrader - sizer

出處:https://ithelp.ithome.com.tw/articles/10279754

之前有介紹過,如果我們下單除了股價以外,還有一個很重要的因素就是要買幾股,有些時候,我們的策略可能會需要不同的買入數量,Backtrader 也有一個物件 sizer 可以提供相關的彈性,如果預設的沒有合適的,也可以自訂義一個,以下先介紹內建的一些 sizer

FixedSize

顧名思義就是固定的數量 參數:

  • stake { default 1 }: 固定數量的股數
  • tranches { default 1 }: 只執行 stake 的幾分之一 最後的數量 = stake / tranches
cerebro.addsizer(bt.sizers.SizerFix, stake = 1000)

FixedReverser

一樣是固定數量,只是在賣的時候,會賣出 2 倍的庫存,也就是把正的庫存賣成負的庫存 參數:

  • stake { default 1 } 固定的股數
cerebro.addsizer(bt.sizers.FixedRevert, stake = 1000)

PercentSizer

使用一定比例的帳戶餘額去買進 參數:

  • percents { default 20 }: 20%
cerebro.addsizer(bt.sizers.PercentSizer, percents = 80)

AllInSizer

基本上和 PercentSizer 一樣,只是預設是 100% 的帳戶餘額去買進股票,另一個差別就是 All in 聽起來比較霸氣。 這裡有一個要注意的是,AllIn 是以當天的收盤價去算要買的股數,可是在執行買入的時候,是隔天的開盤價,所以隔天是漲的話,就會造成餘額不足,買入失敗喔

cerebro.addsizer(bt.sizers.AllInSizer)

其它

PercentSizerInt, AllInSizerInt,這兩個看說明是說在回傳數量的時候會轉成整數,不過我真正去執行的結果,兩個都是一樣的,所以暫時看不出差別

自訂義 sizer

要自訂義 sizer 也很簡單

  • 訂義一個 sizer 的 class 繼承 backtrader.Sizer 繼承後可以使用 self.strategy 和 self.broker 來取得相關資料
    • 取得庫存 self.strategy.getposition(data)
    • 取得目前淨值 self.broker.getvalue() (或是 self.stratgy.broker.getvalue())
  • 覆寫 _getsizing(self, comminfo, cash, data, isbuy)
    • comminfo: 手續費相關資訊
    • cash:目前帳戶餘額
    • data: 目前的操作(買入/賣出)資料
    • isbuy: 是否為買入(True)

例如:改寫 percent 變成可以買的最大的張數 ( 1000 股 )

import backtrader as bt
import math
class PercentBoardSizer(bt.Sizer):
    params = (
        ('percents', 20),
    )

    def _getsizing(self, comminfo, cash, data, isbuy):
        position = self.broker.getposition(data)
        if not position:
            size = cash / data.close[0] * (self.p.percents / 100)
            
            if size < 1000:
                # 小於 1000 股,就不買
                size = 0
            else:
                size = math.floor(size / 1000) * 1000
        else:
            size = position.size

        size = int(size)
        
        return size

class AllInBoardSizer(PercentBoardSizer):
    params = (
        ('percents', 100),
    )

使用方法

  • 在 strategy 中:

    • def setsizer(self, sizer): 可以取得已經初始化的 sizer
    • def getsizer(self): 回傳目前使用的 sizer
    • sizer 屬性可以直接進行 get/set (前幾天的範例就是使用這個)
  • 使用 cerebro: 目前看來,如果 strategy 和 cerebro 都有設定的話,會以 cerebro 為主,cerebro 有兩個方法可以設定

    • addsizer(sizerClass, *args, **kwargs): 指定所有的 strategy 使用的 sizer
    • addsizer_byidx(idx, sizerClass, *args, **kwargs): 根據不同的 strategy inx 來使用不同的 sizer ex:
    cerebro = bt.Cerebro()
    # 預設的 sizer
    cerebro.addsizer(bt.sizers.SizerFix, stake = 1000)
    
    # 這樣就可以針對不同的 strategy 來設定不同的 sizer
    idx = cerebro.addstrategy(TestStrategy)
    cerebro.addsizer_byidx(idx, bt.sizers.SizerFix, stake = 5)
    

Python 回測框架(六)Analyzers

出處:https://stockbuzzai.wordpress.com/2019/07/29/python-%e5%9b%9e%e6%b8%ac%e6%a1%86%e6%9e%b6%ef%bc%88%e5%85%ad%ef%bc%89analyzers/

在跑完策略回測之後,只看淨值曲線圖通常是很難確實分析策略的優劣以及缺失之處。因此 Backtrader 提供了 Analyzers 這組工具來產生一些分析的數據,協助使用者來優化他們的策略。我們先來看下面這段程式碼:

from datetime import datetime
import backtrader
import math
 
 
class AllSizer(backtrader.Sizer):
    def _getsizing(self, comminfo, cash, data, isbuy):
        if isbuy:
            return math.floor(cash/data.high)
        else:
            return self.broker.getposition(data)
 
 
class SmaCross(backtrader.SignalStrategy):
    def __init__(self):
        sma10 = backtrader.ind.SMA(period=10)
        sma30 = backtrader.ind.SMA(period=30)
        crossover = backtrader.ind.CrossOver(sma10, sma30)
        self.signal_add(backtrader.SIGNAL_LONG, crossover)
 
        self.setsizer(AllSizer())
 
 
cerebro = backtrader.Cerebro()
data = backtrader.feeds.YahooFinanceData(dataname='MSFT',
                                         fromdate=datetime(2011, 1, 1),
                                         todate=datetime(2012, 12, 31))
  
cerebro.adddata(data)
cerebro.addstrategy(SmaCross)
cerebro.addanalyzer(backtrader.analyzers.SharpeRatio, _name = 'SR', timeframe=backtrader.TimeFrame.Years)
results = cerebro.run()
print('Sharpe Ratio:', results[0].analyzers.SR.get_analysis()) 

分析工具:SharpeRatio, DrawDown, TimeReturn

這是一個簡單策略,我們根據 10 日均線和 30 日均線的狀態來決定買賣。在這裡我們想要關注夏普比率的資訊,因此我們添加了下列的程式碼:

cerebro.addanalyzer(backtrader.analyzers.SharpeRatio, _name = 'SR', timeframe=backtrader.TimeFrame.Years)
results = cerebro.run()
print('Sharpe Ratio:', results[0].analyzers.SR.get_analysis()) 

我們新增了一個 Sharpe Ratio 的 Analyzer,名字為 SR。同時我們利用 timeframe=backtrader.TimeFrame.Years 把分析的時間單位設定為年。

接著利用 cerebro.run() 來執行模擬,並用 results 來接 cerebro 回傳的結果。因為我們這邊只有一個 Strategy,所以直接使用 results[0] 的結果就可以了。比較特別是 results[0].analyzers.SR.get_analysis() 中是使用前面設定的 SR 當呼叫的名稱,這裡要特別注意。

執行之後我們會得到這個結果:

Sharpe Ratio: OrderedDict([('sharperatio', -0.5071606143998728)])

於是我們就得到 Sharpe Ratio 的值了。另外將 timeframe 的參數改為 backtrader.TimeFrame.Months,我們就會得到以月為單位計算的結果。

Sharpe Ratio: OrderedDict([('sharperatio', -0.06338628736023329)])

另外 Analyzer 其實不是一次只能只用一個,也能一次使用多個,如下面的程式碼,我們同時使用了 DrawDown、TimeReturn 和 SharpeRatio 三種 Analyzers:

from datetime import datetime
import backtrader
import math
 
 
class AllSizer(backtrader.Sizer):
    def _getsizing(self, comminfo, cash, data, isbuy):
        if isbuy:
            return math.floor(cash/data.high)
        else:
            return self.broker.getposition(data)
 
 
class SmaCross(backtrader.SignalStrategy):
    def __init__(self):
        sma10 = backtrader.ind.SMA(period=10)
        sma30 = backtrader.ind.SMA(period=30)
        crossover = backtrader.ind.CrossOver(sma10, sma30)
        self.signal_add(backtrader.SIGNAL_LONG, crossover)
 
        self.setsizer(AllSizer())
 
 
cerebro = backtrader.Cerebro()
data = backtrader.feeds.YahooFinanceData(dataname='MSFT',
                                         fromdate=datetime(2011, 1, 1),
                                         todate=datetime(2012, 12, 31))
 
cerebro.adddata(data)
cerebro.addstrategy(SmaCross)
cerebro.addanalyzer(backtrader.analyzers.SharpeRatio, _name = 'SR', timeframe=backtrader.TimeFrame.Years)
cerebro.addanalyzer(backtrader.analyzers.DrawDown, _name = 'DW')
cerebro.addanalyzer(backtrader.analyzers.TimeReturn, _name = 'TR', timeframe=backtrader.TimeFrame.Months)
results = cerebro.run()
print('Sharpe Ratio:', results[0].analyzers.SR.get_analysis())
print('Max DrawDown:', results[0].analyzers.DW.get_analysis().max)
for date, value in  results[0].analyzers.TR.get_analysis().items():
     print(date, value)  

DrawDown 是計算回撤率的工具,回傳結果中的 max 則是記錄最大回撤率,結果如下:

Max DrawDown: AutoOrderedDict([('len', 198), ('drawdown', 17.71801465164528), ('moneydown', 1976.4599999999991)]) 

這裡我們可以看到,最大回撤總共持續了 198 天,下跌了 19.71%,損失了 1976.46 美元。這可以提供我們在設計停損時的一些依據。

而 TimeReturn 則是計算收益的工具,回傳的結果是一個排序好的 Dict,所以我們把它依序列印出來,結果如下:

2011-01-31 00:00:00 0.0
2011-02-28 00:00:00 0.0
2011-03-31 00:00:00 0.0
2011-04-30 00:00:00 -0.010273999999999783
2011-05-31 00:00:00 -0.024536083724182478
2011-06-30 00:00:00 0.027335665943681864
2011-07-31 00:00:00 0.05368343259399522
2011-08-31 00:00:00 -0.08904885568349952
2011-09-30 00:00:00 -0.042906886971318725
2011-10-31 00:00:00 -0.023389784748569564
2011-11-30 00:00:00 0.0
2011-12-31 00:00:00 -0.0018429968927521356
2012-01-31 00:00:00 0.13663358533688363
2012-02-29 00:00:00 0.0816284079934626
2012-03-31 00:00:00 0.016520373448353
2012-04-30 00:00:00 -0.03730546609310592
2012-05-31 00:00:00 -0.03724447064658454
2012-06-30 00:00:00 -0.011841779134246666
2012-07-31 00:00:00 -0.032471926911606275
2012-08-31 00:00:00 0.017043167076716603
2012-09-30 00:00:00 -0.034276837694325546
2012-10-31 00:00:00 -0.0023662293731249173
2012-11-30 00:00:00 0.0
2012-12-31 00:00:00 -0.03251581227796396 

由此我們可以分析每個月的報酬狀況。

除了這些工具之外,Backtrader 其實還有提供很多其他的分析工具,因為分析工具眾多,這裡就不一一介紹,有興趣的朋友可以去 Backtrader 的網頁上看看: https://www.backtrader.com/docu/analyzers-reference/

多股組合操作

多股組合操作,是一種高級的操作模式。多股組合操作通常有兩種模式,一種是定期調倉,即定期再平衡,比如每週1調倉,或每月1號調倉等。另一種是非定期調倉,比如每日判斷,進行調倉,或者每隔n天進行調倉。

定期調倉(再平衡)通常通過定時器timer來進入調倉邏輯,而非定期調倉通常通過傳統的策略next方法進入調倉邏輯。當然,這並非絕對。

這兩種多股調倉操作,都不能用backtrader內建的自動確定最小期的方法來做(比如為了求20日均線,自動跳過前20個bar),因為有些股票有交易的日期很靠後,它的最小期很大,其他股票也會採用這個最小期,這會導致其他股票浪費最小期前的資料,因此,必須自己控制最小期,也就是prenext方法裡必須寫上self.next()直接跳轉到next。然後如果用到了比如5日均線這樣的指標,你要自己判斷資料對象線長度是否夠長。

儘管網上有一個用backtrader執行多股組合回測的案例,但並未很好地處理好多股回測中的一些問題。本文將給出完善的處理方案。

(基於next的非定期再平衡的策略實現請參考我們編寫的教學和視訊課程)

本文介紹基於定時器timer的多股定期再平衡策略的實現.

本案例的目的是介紹使用backtrader進行組合管理時,要注意的一些技術要點,策略本身僅供參考。策略的大致邏輯如下:每年5月1日,9月1日,11月1日進行組合再平衡操作(若該日休市,則順延到開市日進行再平衡操作)。

首先載入一組股票(股票池),在再平衡日,從股票池挑出至少上市3年,且淨資產收益率roe>0.1,市盈率 pe在0到100間的股票,這組選出的股票再按成交量從大到小排序,選出前100隻股票(如果選出的股票少於100隻,則按實際來),將全部帳戶價值按等比例分配買入這些股票。

該策略反應瞭如下幾個技術要點,把這些要點整明白,基本上就可用於實戰了,程式碼更詳細的解讀特別是定時器timer的用法參考我們編寫的教學和視訊課程

1 擴展PandasData類

2 第一個資料應該對應指數,作為時間基準

3 資料預處理:刪除原始資料中無交易的及缺指標的記錄

4 先平倉再執行後續買賣

5 下單量的計算方法

6 如何保證先賣後買以空出資金

7 怎樣按明日開盤價計算下單數量

8 為行情資料對象提供名字

9 買賣數量如何設為100的整數倍

10 設定符合中國股市的佣金模式,考慮印花稅

11 漲跌停板的處理

# 考慮中國佣金,下單量100的整數倍,漲跌停板,滑點
# 考慮一個技術指標,展示怎樣處理最小期問題

from datetime import datetime, time
from datetime import timedelta
import pandas as pd
import numpy as np
import backtrader as bt
import os.path  # 管理路徑
import sys  # 發現指令碼名字(in argv[0])
import glob
from backtrader.feeds import PandasData  # 用於擴展DataFeed

# 建立新的data feed類


class PandasDataExtend(PandasData):
    # 增加線
    lines = ("pe", "roe", "marketdays")
    params = (
        ("pe", 15),
        ("roe", 16),
        ("marketdays", 17),
    )  # 上市天數


class stampDutyCommissionScheme(bt.CommInfoBase):
    """
    本佣金模式下,買入股票僅支付佣金,賣出股票支付佣金和印花稅.    
    """

    params = (
        ("stamp_duty", 0.005),  # 印花稅率
        ("commission", 0.001),  # 佣金率
        ("stocklike", True),
        ("commtype", bt.CommInfoBase.COMM_PERC),
    )

    def _getcommission(self, size, price, pseudoexec):
        """
        If size is greater than 0, this indicates a long / buying of shares.
        If size is less than 0, it idicates a short / selling of shares.
        """

        if size > 0:  # 買入,不考慮印花稅
            return size * price * self.p.commission
        elif size < 0:  # 賣出,考慮印花稅
            return -size * price * (self.p.stamp_duty + self.p.commission)
        else:
            return 0  # just in case for some reason the size is 0.


class Strategy(bt.Strategy):
    params = dict(
        rebal_monthday=[1], num_volume=100, period=5,  # 每月1日執行再平衡  # 成交量取前100名
    )

    # 日誌函數
    def log(self, txt, dt=None):
        # 以第一個資料data0,即指數作為時間基準
        dt = dt or self.data0.datetime.date(0)
        print("%s, %s" % (dt.isoformat(), txt))

    def __init__(self):

        self.lastRanks = []  # 上次交易股票的列表
        # 0號是指數,不進入選股池,從1號往後進入股票池
        self.stocks = self.datas[1:]
        # 記錄以往訂單,在再平衡日要全部取消未成交的訂單
        self.order_list = []

        # 移動平均線指標
        self.sma = {d: bt.ind.SMA(d, period=self.p.period) for d in self.stocks}

        # 定時器
        self.add_timer(
            when=bt.Timer.SESSION_START,
            monthdays=self.p.rebal_monthday,  # 每月1號觸發再平衡
            monthcarry=True,  # 若再平衡日不是交易日,則順延觸發notify_timer
        )

    def notify_timer(self, timer, when, *args, **kwargs):

        # 只在5,9,11月的1號執行再平衡
        if self.data0.datetime.date(0).month in [5, 9, 11]:
            self.rebalance_portfolio()  # 執行再平衡

    # def next(self):

    #     print('next 帳戶總值', self.data0.datetime.datetime(0), self.broker.getvalue())
    #     for d in self.stocks:
    #         if(self.getposition(d).size!=0):
    #             print(d._name, '持倉' ,self.getposition(d).size)

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # 訂單狀態 submitted/accepted,無動作
            return

        # 訂單完成
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    "買單執行,%s, %.2f, %i"
                    % (order.data._name, order.executed.price, order.executed.size)
                )

            elif order.issell():
                self.log(
                    "賣單執行, %s, %.2f, %i"
                    % (order.data._name, order.executed.price, order.executed.size)
                )

        else:
            self.log(
                "訂單作廢 %s, %s, isbuy=%i, size %i, open price %.2f"
                % (
                    order.data._name,
                    order.getstatusname(),
                    order.isbuy(),
                    order.created.size,
                    order.data.open[0],
                )
            )

    # 記錄交易收益情況
    def notify_trade(self, trade):
        if trade.isclosed:
            print(
                "毛收益 %0.2f, 扣傭後收益 % 0.2f, 佣金 %.2f, 市值 %.2f, 現金 %.2f"
                % (
                    trade.pnl,
                    trade.pnlcomm,
                    trade.commission,
                    self.broker.getvalue(),
                    self.broker.getcash(),
                )
            )

    def rebalance_portfolio(self):
        # 從指數取得當前日期
        self.currDate = self.data0.datetime.date(0)
        print("rebalance_portfolio currDate", self.currDate, len(self.stocks))

        # 如果是指數的最後一本bar,則退出,防止取下一日開盤價越界錯
        if len(self.datas[0]) == self.data0.buflen():
            return

        # 取消以往所下訂單(已成交的不會起作用)
        for o in self.order_list:
            self.cancel(o)
        self.order_list = []  # 重設訂單列表

        # for d in self.stocks:
        #     print('sma', d._name, self.sma[d][0],self.sma[d][1], d.marketdays[0])

        # 最終標的選取過程
        # 1 先做排除篩選過程
        self.ranks = [
            d
            for d in self.stocks
            if len(d) > 0 and d.marketdays > 3 * 365  # 重要,到今日至少要有一根實際bar  # 到今天至少上市
            # 今日未停牌 (若去掉此句,則今日停牌的也可能進入,並下訂單,次日若復牌,則次日可能成交)(假設原始資料中已刪除無交易的記錄)
            and d.datetime.date(0) == self.currDate
            and d.roe >= 0.1
            and d.pe < 100
            and d.pe > 0
            and len(d) >= self.p.period  # 最小期,至少需要5根bar
            and d.close[0] > self.sma[d][1]
        ]

        # 2 再做排序挑選過程
        self.ranks.sort(key=lambda d: d.volume, reverse=True)  # 按成交量從大到小排序
        self.ranks = self.ranks[0 : self.p.num_volume]  # 取前num_volume名

        if len(self.ranks) == 0:  # 無股票選中,則返回
            return

        # 3 以往買入的標的,本次不在標的中,則先平倉
        data_toclose = set(self.lastRanks) - set(self.ranks)
        for d in data_toclose:
            print("sell 平倉", d._name, self.getposition(d).size)
            o = self.close(data=d)
            self.order_list.append(o)  # 記錄訂單

        # 4 本次標的下單
        # 每隻股票買入資金百分比,預留2%的資金以應付佣金和計算誤差
        buypercentage = (1 - 0.02) / len(self.ranks)

        # 得到目標市值
        targetvalue = buypercentage * self.broker.getvalue()
        # 為保證先賣後買,股票要按持倉市值從大到小排序
        self.ranks.sort(key=lambda d: self.broker.getvalue([d]), reverse=True)
        self.log(
            "下單, 標的個數 %i, targetvalue %.2f, 當前總市值 %.2f"
            % (len(self.ranks), targetvalue, self.broker.getvalue())
        )

        for d in self.ranks:
            # 按次日開盤價計算下單量,下單量是100的整數倍
            size = int(
                abs((self.broker.getvalue([d]) - targetvalue) / d.open[1] // 100 * 100)
            )
            validday = d.datetime.datetime(1)  # 該股下一實際交易日
            if self.broker.getvalue([d]) > targetvalue:  # 持倉過多,要賣
                # 次日跌停價近似值
                lowerprice = d.close[0] * 0.9 + 0.02

                o = self.sell(
                    data=d,
                    size=size,
                    exectype=bt.Order.Limit,
                    price=lowerprice,
                    valid=validday,
                )
            else:  # 持倉過少,要買
                # 次日漲停價近似值
                upperprice = d.close[0] * 1.1 - 0.02
                o = self.buy(
                    data=d,
                    size=size,
                    exectype=bt.Order.Limit,
                    price=upperprice,
                    valid=validday,
                )

            self.order_list.append(o)  # 記錄訂單

        self.lastRanks = self.ranks  # 跟蹤上次買入的標的


##########################
# 主程序開始
#########################
cerebro = bt.Cerebro(stdstats=False)
cerebro.addobserver(bt.observers.Broker)
cerebro.addobserver(bt.observers.Trades)
# cerebro.broker.set_coc(True)  # 以訂單建立日的收盤價成交
# cerebro.broker.set_coo(True) # 以次日開盤價成交


datadir = "./dataswind"  # 資料檔案位於本指令碼所在目錄的data子目錄中
datafilelist = glob.glob(os.path.join(datadir, "*"))  # 資料檔案路徑列表

maxstocknum = 20  # 股票池最大股票數目
# 注意,排序第一個檔案必須是指數資料,作為時間基準
datafilelist = datafilelist[0:maxstocknum]  # 擷取指定數量的股票池
print(datafilelist)
# 將目錄datadir中的資料檔案載入進系統


for fname in datafilelist:

    df = pd.read_csv(fname, skiprows=0, header=0,)  # 不忽略行  # 列頭在0行
    # df = df[~df['交易狀態'].isin(['停牌一天'])]  # 去掉停牌日記錄
    df["date"] = pd.to_datetime(df["date"])  # 轉成日期類型
    df = df.dropna()

    # print(df.info())
    # print(df.head())

    data = PandasDataExtend(
        dataname=df,
        datetime=0,  # 日期列
        open=2,  # 開盤價所在列
        high=3,  # 最高價所在列
        low=4,  # 最低價所在列
        close=5,  # 收盤價價所在列
        volume=6,  # 成交量所在列
        pe=7,
        roe=8,
        marketdays=9,
        openinterest=-1,  # 無未平倉量列
        fromdate=datetime(2002, 4, 1),  # 起始日2002, 4, 1
        todate=datetime(2015, 12, 31),  # 結束日 2015, 12, 31
        plot=False,
    )
    ticker = fname[-13:-4]  # 從檔案路徑名取得股票程式碼

    cerebro.adddata(data, name=ticker)


cerebro.addstrategy(Strategy)
startcash = 10000000
cerebro.broker.setcash(startcash)
# 防止下單時現金不夠被拒絕。只在執行時檢查現金夠不夠。
cerebro.broker.set_checksubmit(False)
comminfo = stampDutyCommissionScheme(stamp_duty=0.001, commission=0.001)
cerebro.broker.addcommissioninfo(comminfo)
results = cerebro.run()
print("最終市值: %.2f" % cerebro.broker.getvalue())
# cerebro.plot()

以上這個策略,其實也可以通過next方法進入策略邏輯,具體程式碼和詳情請參考我們的教學:

均線交叉策略

出處:https://ithelp.ithome.com.tw/articles/10242427

前面幾個策略的回測方式,是自己刻一個回測函數,其實是挺麻煩的,還好因為做量化的大家都有回測的需求,所以早就有人開發回測框架拉,就跟網頁開發使用的前端框架或後端框架類似,已經把常用到的功能都寫成模組囉,非常好用。

以Python為基礎的回測框架,包含vnpy、zipline、backtrader...等等,這次先來介紹一下backtrader,因為它用起來蠻直覺的,一些函數也都蠻口語化的,都是很容易懂的英文命名。

Backtrader做回測會用到的元素

就跟一般做回測,通常會需要:OHLC資料、交易策略、回測模組、分析工具。下面就用前一篇寫的均線交叉策略來說明,怎麼使用backtrader做回測:

1. 餵資料(Data Feeds)

關於data feeds的用法,官網的document有列出蠻多種取得資料的方式,這邊就先用常用的Yahoo Finance的資料。

# data feeds
import math
import yfinance as yf
import datetime
import backtrader as bt
import backtrader.feeds as btfeeds


# 從Yahoo Finance取得資料
data = bt.feeds.PandasData(
    dataname=yf.download("SPY", "2015-07-06", "2023-01-01", auto_adjust=True)
)

2. 撰寫策略

# sma cross strategy
class SmaCross(bt.Strategy):
    # 交易紀錄
    def log(self, txt, dt=None):
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))
    
    # 設定交易參數
    params = dict(
        ma_period_short=5,
        ma_period_long=10
    )

    def __init__(self):
        # 均線交叉策略
        sma1 = bt.ind.SMA(period=self.p.ma_period_short)
        sma2 = bt.ind.SMA(period=self.p.ma_period_long)
        self.crossover = bt.ind.CrossOver(sma1, sma2)
        
        # 使用自訂的sizer函數,將帳上的錢all-in
        self.setsizer(sizer())
        
        # 用開盤價做交易
        self.dataopen = self.datas[0].open

    def next(self):
        # 帳戶沒有部位
        if not self.position:
            # 5ma往上穿越20ma
            if self.crossover > 0:
                # 印出買賣日期與價位
                self.log('BUY ' + ', Price: ' + str(self.dataopen[0]))
                # 使用開盤價買入標的
                self.buy(price=self.dataopen[0])
        # 5ma往下穿越20ma
        elif self.crossover < 0:
            # 印出買賣日期與價位
            self.log('SELL ' + ', Price: ' + str(self.dataopen[0]))
            # 使用開盤價賣出標的
            self.close(price=self.dataopen[0])

# 計算交易部位
class sizer(bt.Sizer):
    def _getsizing(self, comminfo, cash, data, isbuy):
        if isbuy:
            return math.floor(cash/data[1])
        else:
            return self.broker.getposition(data)

3. 執行回測 and 顯示分析圖表

cerebro是backtrader回測模組的名稱,將資料、策略丟給cerebro之後,就可以執行回測並作圖了。

# 初始化cerebro
cerebro = bt.Cerebro()
# feed data
cerebro.adddata(data)
# add strategy
cerebro.addstrategy(SmaCross)
# run backtest
cerebro.run()
# plot diagram
cerebro.plot()
# data feeds
import math
import yfinance as yf
import datetime
import backtrader as bt
import backtrader.feeds as btfeeds


# sma cross strategy
class SmaCross(bt.Strategy):
    # 交易紀錄
    def log(self, txt, dt=None):
        dt = dt or self.datas[0].datetime.date(0)
        print("%s, %s" % (dt.isoformat(), txt))

    # 設定交易參數
    params = dict(ma_period_short=5, ma_period_long=10)

    def __init__(self):
        # 均線交叉策略
        sma1 = bt.ind.SMA(period=self.p.ma_period_short)
        sma2 = bt.ind.SMA(period=self.p.ma_period_long)
        self.crossover = bt.ind.CrossOver(sma1, sma2)

        # 使用自訂的sizer函數,將帳上的錢all-in
        self.setsizer(sizer())

        # 用開盤價做交易
        self.dataopen = self.datas[0].open

    def next(self):
        # 帳戶沒有部位
        if not self.position:
            # 5ma往上穿越20ma
            if self.crossover > 0:
                # 印出買賣日期與價位
                self.log("BUY " + ", Price: " + str(self.dataopen[0]))
                # 使用開盤價買入標的
                self.buy(price=self.dataopen[0])
        # 5ma往下穿越20ma
        elif self.crossover < 0:
            # 印出買賣日期與價位
            self.log("SELL " + ", Price: " + str(self.dataopen[0]))
            # 使用開盤價賣出標的
            self.close(price=self.dataopen[0])


# 計算交易部位
class sizer(bt.Sizer):
    def _getsizing(self, comminfo, cash, data, isbuy):
        if isbuy:
            return math.floor(cash / data[1])
        else:
            return self.broker.getposition(data)


# 從Yahoo Finance取得資料
data = bt.feeds.PandasData(
    dataname=yf.download("SPY", "2015-07-06", "2023-01-01", auto_adjust=True)
)


## 執行回測 and 顯示分析圖表

# 初始化cerebro
cerebro = bt.Cerebro()
# feed data
cerebro.adddata(data)
# add strategy
cerebro.addstrategy(SmaCross)
# run backtest
cerebro.run()
# plot diagram
cerebro.plot()

執行程式後,會顯示下面的資料及圖表:

  • 分析圖表 如下圖,預設的起始資金是10000元,期末資產淨值是11238,所以報酬率大概是12.38%,這張圖有四個區塊可以解釋一下,分別從上到下說明:

img

  1. 第1區塊:藍色代表資產淨值,紅色代表帳戶現金,所以當紅色線突然掉下去的時候,就代表買入股票,上升就代表賣出股票。
  2. 第2區塊:代表每次賣出的獲利情況,藍色點點表示賺錢,紅色點點表示虧錢。
  3. 第3區塊:顯示長短均線、買賣點、股價走勢、交易量,從這張圖可以大概看出來買賣的時間點與走勢的相對關係。
  4. 第4區塊:cross over訊號出現的點,可以仔細看到,第一個訊號是賣出訊號(線圖向下凸),但是因為帳上沒有部位,所以沒有賣出動作。

本篇總結 這篇就大概寫了一下backtrader怎麼使用,當然它的功能是非常強大的,還有許多功能是我沒接觸過的,大家都可以google或看document去把玩把玩,蠻有趣的。

唐奇安通道策略

通道策略

要講通道策略,直接看圖最快,下面是維基百科裡面的圖: img

從上圖可以看到,白色的線是股價走勢,上下有兩條黃色的線包覆中間的白色線,包圍的區間看起來就像是一個通道,所以叫做通道策略,上下兩條黃線則是稱為上下軌。

計算這兩條黃色線的方式有很多不同的方式,本篇介紹其中一種叫做唐奇安通道(Donchian Channel),唐奇安通道的上下軌計算方式為,上軌為過去一段時間內的最高價,下軌為過去一段時間內的最低價。

若股價超過上軌,可能表示多頭較強勢,所以產生買進訊號,若股價低於下軌,可能表示空頭較強勢,所以產生賣出訊號。至於這過去一段時間要採用多長的時間,會跟商品性質或是交易頻率比較有相關。

開始寫策略吧

寫唐奇安通道策略大概需要三個步驟:

  • 計算通道上下軌
  • 撰寫交易策略
  • 執行回測

計算通道上下軌

因為上下軌是用來買進賣出的指標,因此可以使用backtrader的Indicators,官網文件有非常詳細的介紹。下面程式碼是官網的範例,由於實在蠻複雜的,就一行一行來解釋:

from datetime import datetime
import yfinance as yf
import backtrader as bt


# 定義一個Indicator物件
class DonchianChannels(bt.Indicator):
    # 這個物件的別名,所以後面我們可以用DCH/DonchianChannel來呼叫這個指標
    alias = ('DCH', 'DonchianChannel',)
    
    # 三條線分別代表唐奇安通道中的 中軌(上軌加下軌再除以2)、上軌、下軌
    lines = ('dcm', 'dch', 'dcl',)  # dc middle, dc high, dc low
    
    # 軌道的計算方式:用過去20天的資料來計算,所以period是20,lookback的意思是要不要將今天的資料納入計算,由於唐奇安通道是取過去20天的最高或最低,所以一定不能涵蓋今天,不然永遠不會有訊號出現,所以要填-1(從前一天開始算20天)
    params = dict(
        period=20,
        lookback=-1,  # consider current bar or not
    )
    
    # 是否要將Indicators另外畫一張圖,然而通道線通常都是跟股價圖畫在同一張,才能看得出相對關係,所以這裡就填subplot=False
    plotinfo = dict(subplot=False)  # plot along with data
    
    # 繪圖設定,ls是line style,'--'代表虛線
    plotlines = dict(
        dcm=dict(ls='--'),  # dashed line
        dch=dict(_samecolor=True),  # use same color as prev line (dcm)
        dcl=dict(_samecolor=True),  # use same color as prev line (dch)
    )
    
    def __init__(self):
        # hi與lo是指每日股價的最高與最低價格
        hi, lo = self.data.high, self.data.low
        
        # 視需求決定是否要從前一天開始讀資料,上面已經定義lookback存在,所以這邊會直接從前一天的資料開始跑
        if self.p.lookback:  # move backwards as needed
            hi, lo = hi(self.p.lookback), lo(self.p.lookback)
        
        # 定義三條線的計算方式
        self.l.dch = bt.ind.Highest(hi, period=self.p.period)
        self.l.dcl = bt.ind.Lowest(lo, period=self.p.period)
        self.l.dcm = (self.l.dch + self.l.dcl) / 2.0  # avg of the above

撰寫交易策略

寫策略的部份就相對簡單很多,第一步把指標帶入,再來就產出訊號拉。

# 撰寫交易策略
class MyStrategy(bt.Strategy):
    def __init__(self):
        # DCH就是上面定義的 DonchianChannels的alias
        self.myind = DCH()

    def next(self):
        if self.data[0] > self.myind.dch[0]:
            self.buy()
        elif self.data[0] < self.myind.dcl[0]:
            self.sell()

執行回測

cerebro = bt.Cerebro()
cerebro.addstrategy(MyStrategy)
cerebro.broker.setcash(1000)
cerebro.broker.setcommission(commission=0.001)

data = bt.feeds.PandasData(
    dataname=yf.download("AAPL", "2019-01-01", "2019-12-31", auto_adjust=True)
)

cerebro.adddata(data)
print('Starting Value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Ending Value: %.2f' % cerebro.broker.getvalue())
cerebro.plot()
from datetime import datetime
import yfinance as yf
import backtrader as bt

# 定義一個Indicator物件
class DonchianChannels(bt.Indicator):
    # 這個物件的別名,所以後面我們可以用DCH/DonchianChannel來呼叫這個指標
    alias = (
        "DCH",
        "DonchianChannel",
    )

    # 三條線分別代表唐奇安通道中的 中軌(上軌加下軌再除以2)、上軌、下軌
    lines = (
        "dcm",
        "dch",
        "dcl",
    )  # dc middle, dc high, dc low

    # 軌道的計算方式:用過去20天的資料來計算,所以period是20,lookback的意思是要不要將今天的資料納入計算,由於唐奇安通道是取過去20天的最高或最低,所以一定不能涵蓋今天,不然永遠不會有訊號出現,所以要填-1(從前一天開始算20天)
    params = dict(period=20, lookback=-1,)  # consider current bar or not

    # 是否要將Indicators另外畫一張圖,然而通道線通常都是跟股價圖畫在同一張,才能看得出相對關係,所以這裡就填subplot=False
    plotinfo = dict(subplot=False)  # plot along with data

    # 繪圖設定,ls是line style,'--'代表虛線
    plotlines = dict(
        dcm=dict(ls="--"),  # dashed line
        dch=dict(_samecolor=True),  # use same color as prev line (dcm)
        dcl=dict(_samecolor=True),  # use same color as prev line (dch)
    )

    def __init__(self):
        # hi與lo是指每日股價的最高與最低價格
        hi, lo = self.data.high, self.data.low

        # 視需求決定是否要從前一天開始讀資料,上面已經定義lookback存在,所以這邊會直接從前一天的資料開始跑
        if self.p.lookback:  # move backwards as needed
            hi, lo = hi(self.p.lookback), lo(self.p.lookback)

        # 定義三條線的計算方式
        self.l.dch = bt.ind.Highest(hi, period=self.p.period)
        self.l.dcl = bt.ind.Lowest(lo, period=self.p.period)
        self.l.dcm = (self.l.dch + self.l.dcl) / 2.0  # avg of the above


# 撰寫交易策略
class MyStrategy(bt.Strategy):
    def __init__(self):
        # DCH就是上面定義的 DonchianChannels的alias
        self.myind = DCH()

    def next(self):
        if self.data[0] > self.myind.dch[0]:
            self.buy()
        elif self.data[0] < self.myind.dcl[0]:
            self.sell()


# 執行回測
cerebro = bt.Cerebro()
cerebro.addstrategy(MyStrategy)
cerebro.broker.setcash(1000)
cerebro.broker.setcommission(commission=0.001)

data = bt.feeds.PandasData(
    dataname=yf.download("AAPL", "2019-01-01", "2019-12-31", auto_adjust=True)
)

cerebro.adddata(data)
print("Starting Value: %.2f" % cerebro.broker.getvalue())
cerebro.run()
print("Ending Value: %.2f" % cerebro.broker.getvalue())
cerebro.plot()

回測2019年的結果(過去績效僅供參考,不保證未來能夠持續獲利): Starting Value: 1000.00 Ending Value: 1400.97 img

從上圖可以看得出來,因為2019年AAPL股價一直上漲的關係,很容易突破前20日的高點,導致綠色的三角形比紅色三角形多。

本篇總結 這篇就簡單的寫了一下用Backtrader回測唐奇安通道策略,又對這個框架多瞭解一些,覺得bt的功能真的超多,好用好用!

Sizers模組

出處:https://ithelp.ithome.com.tw/articles/10243685

前言

前面在使用backtrader的時候,沒有特別針對交易部位的計算作介紹。主要是因為想先簡化實作過程,快速寫完幾個簡單的策略。這篇就要來深入研究,當交易訊號產生的時候,怎麼樣使用backtrader的Sizers模組,撰寫我們要買賣多少部位,不管是all-in、每次買進固定股數...等等,都可以化成一行一行的程式碼,真的是非常神奇呢!

首先,看看document

之前看過很多人寫的Sizers範例,用法真的是千變萬化,所以我覺得要理解他到底有哪些用法,最快速的方式就是去看document

回顧一下 Day10 - backtrader回測框架實作(一)均線交叉策略

def next(self):
        # 帳戶沒有部位
        if not self.position:
            # 5ma往上穿越20ma
            if self.crossover > 0:
                # 印出買賣日期與價位
                self.log('BUY ' + ', Price: ' + str(self.dataopen[0]))
                # 使用開盤價買入標的
                self.buy(price=self.dataopen[0])
        # 5ma往下穿越20ma
        elif self.crossover < 0:
            # 印出買賣日期與價位
            self.log('SELL ' + ', Price: ' + str(self.dataopen[0]))
            # 使用開盤價賣出標的
            self.close(price=self.dataopen[0])

上面這段程式碼中,self.buy的參數只有指定price,其他都是使用預設參數,那麼預設的交易量是多少呢?得要看一下說明書:

class SizerFix(SizerBase):
    params = (('stake', 1),)

上面表示:如果buy跟sell沒有指定size是多少,那就會是1單位。但是我們通常不會只有買賣1單位,所以就需要寫sizer。

如何使用Sizer

這邊就介紹兩個Sizer用法,一種是寫在回測函數內,另一個則是寫在策略函數內:

  • 寫在回測函數(cerebro)內
cerebro = bt.Cerebro()
cerebro.addsizer(bt.sizers.SizerFix, stake=20)

這邊addsizer裡面就是指定買賣的size是固定的,每次交易都是20單位

  • 寫在策略函數內

The Strategy class offers an API: setsizer and getsizer (and a property sizer) to manage the Sizer

上面這句話表示可以在Strategy裡面,用setsizer或getsizer來管理交易量,下面就以之前寫的sma cross解釋setsizer的方式:

# sma cross strategy
class SmaCross(bt.Strategy):
    ...

    def __init__(self):
        # 均線交叉策略
        sma1 = bt.ind.SMA(period=self.p.ma_period_short)
        sma2 = bt.ind.SMA(period=self.p.ma_period_long)
        self.crossover = bt.ind.CrossOver(sma1, sma2)
        
        # 使用自訂的sizer函數,將帳上的錢all-in
        self.setsizer(sizer())
        
    ...

方法相當簡單,就是在init裡面寫setsizer,再定義一個sizer class就完成了。詳細的文件可以看官網的Size Development的部份,這邊就先用all-in法做解說,所謂all-in就是帳上有多少錢就買多少:

# 計算交易部位
class sizer(bt.Sizer):
    def _getsizing(self, comminfo, cash, data, isbuy):
        if isbuy:
            return math.floor(cash/data.open)
        else:
            return self.broker.getposition(data)

Override the method _getsizing(self, comminfo, cash, data, isbuy) 意思就是可以用自己定義的交易量去覆蓋掉原本的method,而上面寫的cash / data.open,就是指用開盤價來計算帳上的cash可以買多少的量,再四捨五入後就是真正交易的量了。

本篇總結 那麼以上就是sizers的介紹,有了sizer,就可以寫各式各樣的交易量配置,資金運用會再更靈活一些,下一篇將介紹Observers,就是回測成果的觀測工具,請繼續收看囉!

Observers模組

還記得在Day10均線交叉策略看到的這張圖嗎?從上到下分成4個區塊(Cash and Value/Trade/BuySell/CrossOver),之前有分別講一下這4個區塊的功能,這篇則是說明這些區塊是怎麼呼叫出來的,以及這些區塊是否可以增加更多資訊或是刪減一些區塊。 img

先來看一下document

All backtrader sample charts have so far had 3 things plotted which seem to be taken for granted because they are not declared anywhere:

  • Cash and Value (what’s happening with the money in the broker)
  • Trades (aka Operations)
  • Buy/Sell Orders

They are Observers and exist within the submodule backtrader.observers. They are there because Cerebro supports a parameter to automatically add (or not) them to the Strategy:

  • stdstats (default: True)

所以如果要把Cash & Value/Trades/BuySell的內容刪除,只要在cerebro的部份修改成下面這行就好:

cerebro = bt.Cerebro(stdstats=False)

就會得到下面這張圖,只剩下股價走勢、長短均線、均線交叉訊號。 img

那如果想要把Cash & Value的部份也納入觀察,可以單獨增加這個區塊,只要在cerebro的部份改成:

import math
cerebro = bt.Cerebro(stdstats=False)
# 加上下面這一行
cerebro.addobserver(bt.observers.Broker)
cerebro.adddata(data)
cerebro.addstrategy(SmaCross)
cerebro.run()
cerebro.plot()

或是再加上DrawDown圖(每日損失):

...
cerebro.addobserver(bt.observers.Broker)
# 加上下面這行
cerebro.addobserver(bt.observers.DrawDown)
...

img

如果想把Drawdown的部份,用文字印出來,則可以在strategy的部份,加上兩行code:

class SmaCross(bt.Strategy):
    ...
    def next(self):
        ...
        # 加上下面這兩行
        self.log('DrawDown: %.2f' % self.stats.drawdown.drawdown[-1])
        self.log('MaxDrawDown: %.2f' % self.stats.drawdown.maxdrawdown[-1])
        ...

執行回測時就會印出每日DrawDon及MaxDrawDown,如下

2019-01-16, DrawDown: 0.00
2019-01-16, MaxDrawDown: 0.00
2019-01-17, DrawDown: 0.00
2019-01-17, MaxDrawDown: 0.00
...
2019-03-29, DrawDown: 1.26
2019-03-29, MaxDrawDown: 1.97
2019-04-01, DrawDown: 1.26
2019-04-01, MaxDrawDown: 1.97
...
2019-12-27, DrawDown: 0.00
2019-12-27, MaxDrawDown: 4.60
2019-12-30, DrawDown: 0.00
2019-12-30, MaxDrawDown: 4.60

從上面幾個例子來看,backtrader可以根據個人需求去調整觀測數據的模板,算是蠻彈性的機制,因為不同策略要看的指標往往會有些不同。

本篇總結 那這篇就先寫到這,backtrader這幾篇寫了交易策略、下單部位控制、數據觀測模板,接下來就可以來介紹python串接券商API下單,因為我自己是用IB(Interactive Brokers)交易,所以會以IB的API作為範例,請繼續收看囉。

整合pyfolio了

出處:https://codeantenna.com/a/n8rxd9EgKz

下面介紹如何在backtrader裡使用pyfolio。

1 安裝pyfolio

必須使用如下命令安裝pyfolio,這樣安裝的是最新版:

pip install git+https://github.com/quantopian/pyfolio 不能使用pip install pyfolio來安裝。很多人整合不了pyfolio,就是因為安裝方式不對。

2 在backtrader中使用pyfolio

樣本程式碼test.ipynb如下

from datetime import datetime
import pandas as pd
import backtrader as bt
import yfinance as yf

# 匯入pyfolio 包
import pyfolio as pf


# 建立策略類
class SmaCross(bt.Strategy):
    # 定義參數
    params = dict(period=5)  # 移動平均期數

    # 日誌函數
    def log(self, txt, dt=None):
        """日誌函數"""
        dt = dt or self.datas[0].datetime.date(0)
        print("%s, %s" % (dt.isoformat(), txt))

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # 訂單狀態 submitted/accepted,無動作
            return
        # 訂單完成
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log("買單執行, %.2f" % order.executed.price)
            elif order.issell():
                self.log("賣單執行, %.2f" % order.executed.price)
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log("訂單 Canceled/Margin/Rejected")

    # 記錄交易收益情況(可省略,默認不輸出結果)
    def notify_trade(self, trade):
        if trade.isclosed:
            print(
                "毛收益 %0.2f, 扣傭後收益 % 0.2f, 佣金 %.2f"
                % (trade.pnl, trade.pnlcomm, trade.commission)
            )

    def __init__(self):
        # 移動平均線指標
        self.move_average = bt.ind.MovingAverageSimple(
            self.data, period=self.params.period
        )
        # 交叉訊號指標
        self.crossover = bt.ind.CrossOver(self.data, self.move_average)

        # sma10 = backtrader.ind.SMA(period=10)
        # sma30 = backtrader.ind.SMA(period=30)
        # crossover = backtrader.ind.CrossOver(sma10, sma30)

    def __bt_to_pandas__(self, btdata, len):
        get = lambda mydata: mydata.get(ago=0, size=len)

        fields = {
            "open": get(btdata.open),
            "high": get(btdata.high),
            "low": get(btdata.low),
            "close": get(btdata.close),
            "volume": get(btdata.volume),
        }
        time = [btdata.num2date(x) for x in get(btdata.datetime)]
        return pd.DataFrame(data=fields, index=time)

    def next(self):
        data = self.__bt_to_pandas__(self.datas[1], len(self.datas[1]))
        print(data)
        if not self.position:  # 還沒有倉位
            # 當日收盤價上穿5日均線,建立買單,買入100股
            if self.crossover > 0:
                self.log("建立買單")
                self.buy(size=100)
        # 有倉位,並且當日收盤價下破5日均線,建立賣單,賣出100股
        elif self.crossover < 0:
            self.log("建立賣單")
            self.sell(size=100)


##########################
# 主程序開始
#########################

# 建立大腦引擎對象
cerebro = bt.Cerebro()


# 建立行情資料對象,載入資料
data = bt.feeds.PandasData(dataname=yf.download("MSFT", "2011-01-01", "2023-01-01"))
# self.datas[0] 日K數據, self.datas[1] 月K數據
data = cerebro.resampledata(data, timeframe=bt.TimeFrame.Months, compression=1)
cerebro.adddata(data)  # 將行情資料對象注入引擎

cerebro.addstrategy(SmaCross)  # 將策略注入引擎

cerebro.broker.setcash(10000.0)  # 設定初始資金

# 加入pyfolio分析者
cerebro.addanalyzer(bt.analyzers.PyFolio, _name="pyfolio")

results = cerebro.run()  # 運行
strat = results[0]
pyfoliozer = strat.analyzers.getbyname("pyfolio")
returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items()
pf.create_full_tear_sheet(returns)

這裡pf.create_full_tear_sheet(returns)中pyfolio需要的收益率returns是日收益率。

如果backtrader原始資料不是日線資料,我估計returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items()中,backtrader返回的returns應該是已經轉換成日收益率了,讀者可以驗證一下告訴我哈。

backtrader resample過程

如何將一個小週期的data 轉換成一個大週期的data?具體如何操作,這個在BT裡面使用resample 完成的。例如days轉換成weeks?

data = btfeeds.BacktraderCSVData(dataname=datapath)
cerebro.adddata(data)  # First add the original data - smaller timeframe
cerebro.resampledata(data, timeframe=tframes[args.timeframe],
                     compression=args.compression)

自動在datas裡面新增這2個data,第二個data在第一個data的基礎上,進行資料的resample.

這裡需要注意:

timeframe:壓縮的週期,只能比被壓縮的資料週期大,當前資料週期是days,那麼timeframe可以是weeks,M,year等。

compression:一個timeframe壓縮成多少個bar,一般是1。當前是5個days bar壓縮成一個week bar.

資料壓縮的過程:

當前的data無法進行一次性的preload,linebuffer 每次向前移動一個單位長度,即每次讀取data的一個資料,然後每次checkover一下是否集齊5根bar:

def _checkbarover(self, data, fromcheck=False, forcedata=None):

    chkdata = DTFaker(data, forcedata) if fromcheck else data

    isover = False
    if not self.componly and not self._barover(chkdata):
        return isover

    if self.subdays and self.p.bar2edge:
        isover = True
    elif not fromcheck:  # fromcheck doesn't increase compcount
        self.compcount += 1
        if not (self.compcount % self.p.compression):
            # boundary crossed and enough bars for compression ... proceed
            isover = True

    return isover

當完成一個週期的壓縮後,isover是true。

資料的壓縮演算法_bar 裡面的:

def bupdate(self, data, reopen=False):
    '''Updates a bar with the values from data

    Returns True if the update was the 1st on a bar (just opened)

    Returns False otherwise
    '''
    if reopen:
        self.bstart()

    self.datetime = data.datetime[0]

    self.high = max(self.high, data.high[0])
    self.low = min(self.low, data.low[0])
    self.close = data.close[0]

    self.volume += data.volume[0]
    self.openinterest = data.openinterest[0]

    o = self.open
    if reopen or not o == o:
        self.open = data.open[0]
        return True  # just opened the bar

    return False

壓縮完成的資料存放在:

for i, dti in enumerate(dts):
    if dti is not None:
        di = datas[i]
        rpi = False and di.replaying   # to check behavior
        if dti > dt0:
            if not rpi:  # must see all ticks ...
                di.rewind()  # cannot deliver yet
            # self._plotfillers[i].append(slen)
        elif not di.replaying:
            # Replay forces tick fill, else force here
            di._tick_fill(force=True)

di._tick_fill:完成對一個bar的填充。

量化交易回測框架Backtrader使用optstrategy最佳化

給策略增加指標後,需要給你指標設定參數,比如SMA設定幾天合適呢,每個股票的週期又都不一樣。總不能一個一個的自己嘗試。Backtrader提供了一個參數最佳化的方法,可以按照給出的範圍來運行,大家可以根據結果尋找最優的均線天數。具體可以參看Backtrader官方文件quickstart

  1. 通過給策略一個範圍值,根據運行結果,找出某適合一隻股票的盤整週期。

通過optstrategy方法,給策略設定範圍值,讓策略逐個執行,對比結果。

"""
Created on Sun Mar 29 12:18:17 2020

@author: horace pei
"""
#############################################################
# import
#############################################################
from __future__ import absolute_import, division, print_function, unicode_literals
import os, sys
import pandas as pd
import backtrader as bt

#############################################################
# global const values
#############################################################
#############################################################
# static function
#############################################################
#############################################################
# class
#############################################################
# Create a Stratey
class TestStrategy(bt.Strategy):
    # 自訂均線的實踐間隔,默認是5天
    params = (
        ("maperiod", 5),
        ("printlog", False),
    )

    def log(self, txt, dt=None, doprint=False):
        """ Logging function for this strategy"""
        if self.params.printlog or doprint:
            dt = dt or self.datas[0].datetime.date(0)
            print("%s, %s" % (dt.isoformat(), txt))

    def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].close
        # To keep track of pending orders
        self.order = None
        # buy price
        self.buyprice = None
        # buy commission
        self.buycomm = None
        # 增加均線,簡單移動平均線(SMA)又稱“算術移動平均線”,是指對特定期間的收盤價進行簡單平均化
        self.sma = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.maperiod
        )

    # 訂單狀態改變回呼方法 be notified through notify_order(order) of any status change in an order
    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return
        # Check if an order has been completed
        # Attention: broker could reject order if not enough cash
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    "BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f"
                    % (order.executed.price, order.executed.value, order.executed.comm)
                )
                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            elif order.issell():
                self.log(
                    "SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f"
                    % (order.executed.price, order.executed.value, order.executed.comm)
                )
            self.bar_executed = len(self)
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log("Order Canceled/Margin/Rejected")
        # Write down: no pending order
        self.order = None

    # 交易狀態改變回呼方法 be notified through notify_trade(trade) of any opening/updating/closing trade
    def notify_trade(self, trade):
        if not trade.isclosed:
            return
        # 每筆交易收益 毛利和淨利
        self.log("OPERATION PROFIT, GROSS %.2f, NET %.2f" % (trade.pnl, trade.pnlcomm))

    def next(self):
        # Simply log the closing price of the series from the reference
        self.log("Close, %.2f" % self.dataclose[0])
        # Check if an order is pending ... if yes, we cannot send a 2nd one
        if self.order:
            return
        # Check if we are in the market(當前帳戶持股情況,size,price等等)
        if not self.position:
            # Not yet ... we MIGHT BUY if ...
            if self.dataclose[0] >= self.sma[0]:
                # 當收盤價,大於等於均線的價格
                # BUY, BUY, BUY!!! (with all possible default parameters)
                self.log("BUY CREATE, %.2f" % self.dataclose[0])
                # Keep track of the created order to avoid a 2nd order
                self.order = self.buy()
        else:
            # Already in the market ... we might sell
            if self.dataclose[0] < self.sma[0]:
                # 當收盤價,小於均線價格
                # SELL, SELL, SELL!!! (with all possible default parameters)
                self.log("SELL CREATE, %.2f" % self.dataclose[0])
                # Keep track of the created order to avoid a 2nd order
                self.order = self.sell()

    def stop(self):
        self.log(
            "(MA Period %2d) Ending Value %.2f"
            % (self.params.maperiod, self.broker.getvalue()),
            doprint=True,
        )


#############################################################
# global values
#############################################################
#############################################################
# global function
#############################################################
def get_dataframe():
    # Get a pandas dataframe
    datapath = "./data/stockinfo.csv"
    tmpdatapath = "./data/stockinfo_tmp.csv"
    print("-----------------------read csv---------------------------")
    dataframe = pd.read_csv(
        datapath, skiprows=0, header=0, parse_dates=True, index_col=0
    )
    dataframe.trade_date = pd.to_datetime(dataframe.trade_date, format="%Y%m%d")
    dataframe["openinterest"] = "0"
    feedsdf = dataframe[
        ["trade_date", "open", "high", "low", "close", "vol", "openinterest"]
    ]
    feedsdf.columns = [
        "datetime",
        "open",
        "high",
        "low",
        "close",
        "volume",
        "openinterest",
    ]
    feedsdf.set_index(keys="datetime", inplace=True)
    feedsdf.iloc[::-1].to_csv(tmpdatapath)
    feedsdf = pd.read_csv(
        tmpdatapath, skiprows=0, header=0, parse_dates=True, index_col=0
    )
    if os.path.isfile(tmpdatapath):
        os.remove(tmpdatapath)
        print(tmpdatapath + " removed!")
    return feedsdf


########################################################################
# main
########################################################################
if __name__ == "__main__":
    # Create a cerebro entity(建立cerebro)
    cerebro = bt.Cerebro()
    # Add a strategy(加入自訂策略,可以設定自訂參數,方便調節)
    cerebro.optstrategy(TestStrategy, maperiod=range(3, 15))
    # Get a pandas dataframe(獲取dataframe格式股票資料)
    feedsdf = get_dataframe()
    # Pass it to the backtrader datafeed and add it to the cerebro(加入資料)
    data = bt.feeds.PandasData(dataname=feedsdf)
    cerebro.adddata(data)
    # Add a FixedSize sizer according to the stake(國內1手是100股,最小的交易單位)
    cerebro.addsizer(bt.sizers.FixedSize, stake=100)
    # Set our desired cash start(給經紀人,可以理解為交易所股票帳戶充錢)
    cerebro.broker.setcash(10000.0)
    # Set the commission - 0.1%(設定交易手續費,雙向收取)
    cerebro.broker.setcommission(commission=0.001)
    # Print out the starting conditions(輸出帳戶金額)
    print("Starting Portfolio Value: %.2f" % cerebro.broker.getvalue())
    # Run over everything(執行回測)
    cerebro.run()
    # Print out the final result(輸出帳戶金額)
    print("Final Portfolio Value: %.2f" % cerebro.broker.getvalue())

分析和說明

通過: cerebro.optstrategy(TestStrategy, maperiod=range(3,15)),來設定3到15天的均線,看看均線時間那個收益最好。

追高進場與加碼,固定停損停利,計算固定資金的數量

from __future__ import absolute_import, division, print_function, unicode_literals

from math import e
import yfinance as yf
import pyfolio
import backtrader as bt
import numpy as np
import warnings
import pandas as pd

warnings.filterwarnings("ignore")

# 計算固定資金的數量
class FixedCash(bt.Sizer):
    def __init__(self, cash=10000):
        self.cash = cash

    def _getsizing(self, comminfo, cash, data, isbuy):
        print(self.cash // data.close[0], data.close[0])
        if isbuy:
            return self.cash // data.close[0]  # 向下取整得到可買入的數量
        else:
            return self.broker.getposition(data).size  # 返回持倉數量


# 固定數量
class FixedSize(bt.Sizer):
    def __init__(self, stake=10000):
        self.stake = stake

    def _getsizing(self, comminfo, cash, data, isbuy):
        return self.stake


# 買總總金%數量
class DynamicRiskSizer(bt.Sizer):
    def _getsizing(self, comminfo, cash, data, isbuy):
        # 查詢當前交易賬戶的總價值
        total_value = self.broker.get_value()

        # 計算每個交易的交易量
        trade_value = total_value * 0.01  # 每個交易的風險百分比為1%
        risk = (
            data.close[0] - data.close[-1] if isbuy else data.close[-1] - data.close[0]
        )
        trade_size = trade_value / abs(risk)

        return int(trade_size)


# 建立一個backtrader回測框架
class Highest_high(bt.Strategy):
    # 設置sma的參數,根據官方照此設置可進行暴力演算,得知何種參數最佳
    params = (
        ("highest", 6),
        ("in_amount", 4),
        ("stoploss", 0.1),
        ("takeprofit", 0.2),
    )

    # 這裡是log,當交易發生時呼叫log函數可以將交易print出來
    def log(self, txt, dt=None):
        """ Logging function fot this strategy"""
        dt = dt or self.datas[0].datetime.date(0)
        # print('%s, %s' % (dt.isoformat(), txt))

    # init定義你會用到的數據
    def __init__(self):
        # 呼叫high序列備用
        self.datahigh = self.datas[0].high
        # 呼叫close序列備用
        self.dataclose = self.datas[0].close
        # 追蹤order、buyprice跟buycomm使用,可用可不用
        self.order = None
        self.buyprice = None
        self.buycomm = None
        # 使用指標套件給的最高價判斷函數Highest
        self.the_highest_high = bt.ind.Highest(
            self.datahigh, period=self.params.highest
        )

    # notify_order當每次有訂單由next偵測出來的條件送出時,會觸發notify_order,好處是顯示出訂單執行的狀況以及偵測是否有資金不足的情況
    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # 當訂單為提交狀態時則不做任何事
            return

        # 當訂單完成時,若為Buy則print出買入狀況;反之亦然
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    "BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f"
                    % (order.executed.price, order.executed.value, order.executed.comm)
                )
                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:
                self.log(
                    "SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f"
                    % (order.executed.price, order.executed.value, order.executed.comm)
                )

        # 當因策略取消或是現今不足訂單被拒絕等狀況則print出訂單取消
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log("Order Canceled/Margin/Rejected")

        # 完成該有的提醒之後則將oder設置回None
        self.order = None

    # notify_trade交易通知,預設如果有倉在手就不做事,如果執行賣出則print出獲利
    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log("OPERATION PROFIT, GROSS %.2f, NET %.2f" % (trade.pnl, trade.pnlcomm))

    # next可以把它想像成一個內建的for loop,他把數據打包好供我們使用
    def next(self):
        # 獲取當前日期和股票的收盤價格
        date = self.datas[0].datetime.date(0).isoformat()
        close = self.datas[0].close[0]

        # 檢查有無pending的訂單
        if self.order:
            return
        # self.position.size獲得目前倉位資訊,當size<指定進場次數時則允許買入
        if self.position.size < self.params.in_amount * 1000:
            # 當現在的高大於前面n根的最高價時準備執行買入
            if self.datahigh > self.the_highest_high[-1]:
                # 紀錄買單提交
                self.log("BUY CREATE, %.2f" % self.dataclose[0])
                # 買進
                self.order = self.buy()
        # 當庫存部位不為0但表有庫存
        if self.position.size != 0:
            # 獲取庫存成本
            costs = self.position.price
            # 當收盤價大於平均成本的10%停利賣出
            if self.dataclose[0] > costs + (costs * self.params.takeprofit):
                self.close()
                self.log("Take Profit, %.2f" % self.dataclose[0])
            # 當收盤價小於平均成本
            elif self.dataclose[0] < costs - (costs * self.params.stoploss):
                self.close()
                self.log("Stop Loss, %.2f" % self.dataclose[0])
        print(
            f"Date: {date}, Closing:{close}, holding of shares:{self.position.size}, total captial:{self.broker.get_value()}"
        )
        input()

    # #回測終止時print出結果
    # def stop(self):
    #     print(f'Fast MA: {self.params.fast_period} | Slow MA: {self.params.slow_period} | End Value: {self.broker.getvalue()}')


if __name__ == "__main__":
    # 創建框架
    cerebro = bt.Cerebro()
    # # 放入策略
    # cerebro.addstrategy(Highest_high)
    # # 放入策略
    cerebro.optstrategy(
        Highest_high,
        highest=range(5, 9),
        in_amount=range(1, 5),
        stoploss=np.arange(0.1, 0.5, 0.1),
        takeprofit=np.arange(0.1, 0.5, 0.1),
    )

    # 使用框架的資料取得函數
    data = bt.feeds.PandasData(
        dataname=yf.download("2317.TW", "2014-01-01", "2023-01-01")
    )
    # 將datafeed餵入框架
    cerebro.adddata(data)
    # 設置起始金額
    cerebro.broker.setcash(1000000.0)
    # 設置一次購買的股數,臺股以1000股為主
    # cerebro.addsizer(bt.sizers.SizerFix, stake=1000)

    cerebro.addsizer(FixedCash)

    # 設置傭金,稍微設置高一點作為滑價付出成本
    cerebro.broker.setcommission(commission=0.0015)

    # ===================Pyfolio===================
    # cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')

    # 在設置完傭金、起始金額以及買入股數之後,我們加入三種分析
    cerebro.addanalyzer(bt.analyzers.SharpeRatio)
    cerebro.addanalyzer(bt.analyzers.Returns)
    cerebro.addanalyzer(bt.analyzers.DrawDown)
    results = cerebro.run(maxcpus=1)
    # 準備list存放每一個參數及結果
    par1, par2, par3, par4, ret, down, sharpe_r = [], [], [], [], [], [], []
    # 迴圈每一個結果
    for strat in results:
        # 因為結果是用list包起來(範例在下註解),所以我們要[0]取值
        # [<backtrader.cerebro.OptReturn object at 0x0000024FF9717CC8>]
        strat = strat[0]
        # get_analysis()獲得值
        a_return = strat.analyzers.returns.get_analysis()
        drawDown = strat.analyzers.drawdown.get_analysis()
        sharpe = strat.analyzers.sharperatio.get_analysis()
        # 依序裝入資料,可用strat.params.xx獲取參數
        par1.append(strat.params.highest)
        par2.append(strat.params.in_amount)
        par3.append(strat.params.stoploss)
        par4.append(strat.params.takeprofit)
        # rtot代表總回報,獲取總回報
        ret.append(a_return["rtot"])
        # 我們關注最大的drawdown,因此如下取值
        down.append(drawDown["max"]["drawdown"])
        # 獲取sharpe ratio
        sharpe_r.append(sharpe["sharperatio"])
    # 組裝成dataframe
    result_df = pd.DataFrame()
    result_df["Highest"] = par1
    result_df["in_amount"] = par2
    result_df["stoploss"] = par3
    result_df["takeprofit"] = par4
    result_df["total profit"] = ret
    result_df["Max Drawdown"] = down
    result_df["Sharpe Ratio"] = sharpe_r
    # 根據總報酬來排列
    result_df = result_df.sort_values(by=["total profit"], ascending=False)
    print(result_df)

    # 畫Kbars
    # cerebro.plot(style='candlestick', barup='red', bardown='green')

    # ===================Pyfolio===================
    # strat = results[0]
    # pyfoliozer = strat.analyzers.getbyname('pyfolio')
    # returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items()

    # # # pyfolio showtime
    # import pyfolio as pf
    # pf.create_full_tear_sheet(
    # returns,
    # positions=positions,
    # transactions=transactions,
    # live_start_date='2018-01-01')  # This date is sample specific)

追高進場與加碼,固定停損停利

from __future__ import absolute_import, division, print_function, unicode_literals

import datetime
from math import e
import yfinance as yf
import os.path
import sys
import pyfolio
import backtrader as bt
import numpy as np
import warnings
import pandas as pd

warnings.filterwarnings("ignore")
# 建立一個backtrader回測框架
class Highest_high(bt.Strategy):
    # 設置sma的參數,根據官方照此設置可進行暴力演算,得知何種參數最佳
    params = (
        ("highest", 6),
        ("in_amount", 4),
        ("stoploss", 0.1),
        ("takeprofit", 0.2),
    )

    # 這裡是log,當交易發生時呼叫log函數可以將交易print出來
    def log(self, txt, dt=None):
        """ Logging function fot this strategy"""
        dt = dt or self.datas[0].datetime.date(0)
        # print('%s, %s' % (dt.isoformat(), txt))

    # init定義你會用到的數據
    def __init__(self):
        # 呼叫high序列備用
        self.datahigh = self.datas[0].high
        # 呼叫close序列備用
        self.dataclose = self.datas[0].close
        # 追蹤order、buyprice跟buycomm使用,可用可不用
        self.order = None
        self.buyprice = None
        self.buycomm = None
        # 使用指標套件給的最高價判斷函數Highest
        self.the_highest_high = bt.ind.Highest(
            self.datahigh, period=self.params.highest
        )

    # notify_order當每次有訂單由next偵測出來的條件送出時,會觸發notify_order,好處是顯示出訂單執行的狀況以及偵測是否有資金不足的情況
    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # 當訂單為提交狀態時則不做任何事
            return

        # 當訂單完成時,若為Buy則print出買入狀況;反之亦然
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    "BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f"
                    % (order.executed.price, order.executed.value, order.executed.comm)
                )
                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:
                self.log(
                    "SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f"
                    % (order.executed.price, order.executed.value, order.executed.comm)
                )

        # 當因策略取消或是現今不足訂單被拒絕等狀況則print出訂單取消
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log("Order Canceled/Margin/Rejected")

        # 完成該有的提醒之後則將oder設置回None
        self.order = None

    # notify_trade交易通知,預設如果有倉在手就不做事,如果執行賣出則print出獲利
    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log("OPERATION PROFIT, GROSS %.2f, NET %.2f" % (trade.pnl, trade.pnlcomm))

    # next可以把它想像成一個內建的for loop,他把數據打包好供我們使用
    def next(self):
        # 檢查有無pending的訂單
        print(
            f"holding of shares:{self.position.size}, total captial:{self.broker.get_value()}"
        )
        if self.order:
            return
        # self.position.size獲得目前倉位資訊,當size<指定進場次數時則允許買入
        if self.position.size < self.params.in_amount * 1000:
            # 當現在的高大於前面n根的最高價時準備執行買入
            if self.datahigh > self.the_highest_high[-1]:
                # 紀錄買單提交
                self.log("BUY CREATE, %.2f" % self.dataclose[0])
                # 買進
                self.order = self.buy()
        # 當庫存部位不為0但表有庫存
        if self.position.size != 0:
            # 獲取庫存成本
            costs = self.position.price
            # 當收盤價大於平均成本的10%停利賣出
            if self.dataclose[0] > costs + (costs * self.params.takeprofit):
                self.close()
                self.log("Take Profit, %.2f" % self.dataclose[0])
            # 當收盤價小於平均成本
            elif self.dataclose[0] < costs - (costs * self.params.stoploss):
                self.close()
                self.log("Stop Loss, %.2f" % self.dataclose[0])

    # #回測終止時print出結果
    # def stop(self):
    #     print(f'Fast MA: {self.params.fast_period} | Slow MA: {self.params.slow_period} | End Value: {self.broker.getvalue()}')


if __name__ == "__main__":
    # 創建框架
    cerebro = bt.Cerebro()
    # # 放入策略
    # cerebro.addstrategy(Highest_high)
    # # 放入策略
    cerebro.optstrategy(
        Highest_high,
        highest=range(5, 9),
        in_amount=range(1, 5),
        stoploss=np.arange(0.1, 0.5, 0.1),
        takeprofit=np.arange(0.1, 0.5, 0.1),
    )

    # 使用框架的資料取得函數
    data = bt.feeds.PandasData(
        dataname=yf.download("2317.TW", "2014-01-01", "2023-01-01")
    )
    # 將datafeed餵入框架
    cerebro.adddata(data)
    # 設置起始金額
    cerebro.broker.setcash(1000000.0)
    # 設置一次購買的股數,臺股以1000股為主
    cerebro.addsizer(bt.sizers.SizerFix, stake=1000)
    # 設置傭金,稍微設置高一點作為滑價付出成本
    cerebro.broker.setcommission(commission=0.0015)

    # ===================Pyfolio===================
    # cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')

    # 在設置完傭金、起始金額以及買入股數之後,我們加入三種分析
    cerebro.addanalyzer(bt.analyzers.SharpeRatio)
    cerebro.addanalyzer(bt.analyzers.Returns)
    cerebro.addanalyzer(bt.analyzers.DrawDown)
    results = cerebro.run(maxcpus=1)
    # 準備list存放每一個參數及結果
    par1, par2, par3, par4, ret, down, sharpe_r = [], [], [], [], [], [], []
    # 迴圈每一個結果
    for strat in results:
        # 因為結果是用list包起來(範例在下註解),所以我們要[0]取值
        # [<backtrader.cerebro.OptReturn object at 0x0000024FF9717CC8>]
        strat = strat[0]
        # get_analysis()獲得值
        a_return = strat.analyzers.returns.get_analysis()
        drawDown = strat.analyzers.drawdown.get_analysis()
        sharpe = strat.analyzers.sharperatio.get_analysis()
        # 依序裝入資料,可用strat.params.xx獲取參數
        par1.append(strat.params.highest)
        par2.append(strat.params.in_amount)
        par3.append(strat.params.stoploss)
        par4.append(strat.params.takeprofit)
        # rtot代表總回報,獲取總回報
        ret.append(a_return["rtot"])
        # 我們關注最大的drawdown,因此如下取值
        down.append(drawDown["max"]["drawdown"])
        # 獲取sharpe ratio
        sharpe_r.append(sharpe["sharperatio"])
    # 組裝成dataframe
    result_df = pd.DataFrame()
    result_df["Highest"] = par1
    result_df["in_amount"] = par2
    result_df["stoploss"] = par3
    result_df["takeprofit"] = par4
    result_df["total profit"] = ret
    result_df["Max Drawdown"] = down
    result_df["Sharpe Ratio"] = sharpe_r
    # 根據總報酬來排列
    result_df = result_df.sort_values(by=["total profit"], ascending=False)
    print(result_df)

    # 畫Kbars
    # cerebro.plot(style='candlestick', barup='red', bardown='green')

    # ===================Pyfolio===================
    # strat = results[0]
    # pyfoliozer = strat.analyzers.getbyname('pyfolio')
    # returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items()

    # # # pyfolio showtime
    # import pyfolio as pf
    # pf.create_full_tear_sheet(
    # returns,
    # positions=positions,
    # transactions=transactions,
    # live_start_date='2018-01-01')  # This date is sample specific)
import backtrader as bt
import yfinance as yf

class MyStrategy(bt.Strategy):
    
    # 設定MA均線的週期
    params = (
        ('ma10', 10),
        ('ma20', 20),
        ('ma40', 40),
        ('take_profit', 0.2), # 設定獲利目標為20%
    )

    def __init__(self):
        self.ma10 = bt.indicators.MovingAverageSimple(self.data.close, period=self.params.ma10)
        self.ma20 = bt.indicators.MovingAverageSimple(self.data.close, period=self.params.ma20)
        self.ma40 = bt.indicators.MovingAverageSimple(self.data.close, period=self.params.ma40)
        
    def next(self):
        # 如果當前價格低於MA10,就買入一次
        if self.data.close[0] < self.ma10[0]:
            self.buy(size=1)
            
        # 如果當前價格低於MA20,再買入一次
        if self.data.close[0] < self.ma20[0]:
            self.buy(size=1)
            
        # 如果當前價格低於MA40,就停止交易並止損
        if self.data.close[0] < self.ma40[0]:
            self.close()
            
        # 如果當前有持倉,計算當前持倉的盈利百分比
        if self.position:
            profit = (self.data.close[0] - self.position.price) / self.position.price

            # 如果當前持倉的盈利達到設定的獲利目標,就賣出並止盈
            if profit >= self.params.take_profit:
                self.close()
            
cerebro = bt.Cerebro()

# 設定初始資金為10000美元
cerebro.broker.setcash(10000)

data = bt.feeds.PandasData(dataname=yf.download("MSFT", "2011-01-01", "2023-01-01"))

# 將數據傳入Cerebro中
cerebro.adddata(data)

# 將策略傳入Cerebro中
cerebro.addstrategy(MyStrategy)

# 運行回測
cerebro.run()

# 打印最終資金餘額
print('Final Balance: %.2f' % cerebro.broker.getvalue())

from datetime import datetime
import pandas as pd
import backtrader as bt
import yfinance as yf

# 匯入pyfolio 包
import pyfolio as pf


# 建立策略類
class SmaCross(bt.Strategy):
    # 定義參數
    params = (
        ("ma10", 10),
        ("ma20", 20),
        ("ma40", 40),
        ("take_profit", 0.2),  # 設定獲利目標為20%
    )

    # 日誌函數
    def log(self, txt, dt=None):
        """日誌函數"""
        dt = dt or self.datas[0].datetime.date(0)
        print("%s, %s" % (dt.isoformat(), txt))

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # 訂單狀態 submitted/accepted,無動作
            return
        # 訂單完成
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log("買單執行, %.2f" % order.executed.price)
            elif order.issell():
                self.log("賣單執行, %.2f" % order.executed.price)
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log("訂單 Canceled/Margin/Rejected")

    # 記錄交易收益情況(可省略,默認不輸出結果)
    def notify_trade(self, trade):
        if trade.isclosed:
            print(
                "毛收益 %0.2f, 扣傭後收益 % 0.2f, 佣金 %.2f"
                % (trade.pnl, trade.pnlcomm, trade.commission)
            )

    def __init__(self):
        self.ma10 = bt.indicators.MovingAverageSimple(
            self.data.close, period=self.params.ma10
        )
        self.ma20 = bt.indicators.MovingAverageSimple(
            self.data.close, period=self.params.ma20
        )
        self.ma40 = bt.indicators.MovingAverageSimple(
            self.data.close, period=self.params.ma40
        )

    def __bt_to_pandas__(self, btdata, len):
        get = lambda mydata: mydata.get(ago=0, size=len)

        fields = {
            "open": get(btdata.open),
            "high": get(btdata.high),
            "low": get(btdata.low),
            "close": get(btdata.close),
            "volume": get(btdata.volume),
        }
        time = [btdata.num2date(x) for x in get(btdata.datetime)]
        return pd.DataFrame(data=fields, index=time)

    def next(self):
        data = self.__bt_to_pandas__(self.datas[1], len(self.datas[1]))
        cash = self.broker.cash
        # print('剩餘現金:', cash)
        # print(data)
        # Get the current position
        pos = self.getposition()
        # Get the position size, 股數
        size = pos.size
        print(
            f"size:{size}, close:{self.data.close[0]}, ma10:{self.ma10[0]}, ma20:{self.ma20[0]}, ma40:{self.ma40[0]}, cash:{cash}"
        )

        # 如果當前價格低於MA10,就買入一次
        if self.data.close[0] < self.ma10[0] and size < 200:
            self.buy(size=10)
            print("close < ma10")

        # 如果當前價格低於MA20,再買入一次
        if self.data.close[0] < self.ma20[0] and size < 200:
            self.buy(size=10)
            print("close < ma20")

        # 如果當前有持倉,計算當前持倉的盈利百分比
        if self.position:
            # 如果當前價格低於MA40,就停止交易並止損
            if self.data.close[0] < self.ma40[0]:
                self.close()

            profit = (self.data.close[0] - self.position.price) / self.position.price

            # 如果當前持倉的盈利達到設定的獲利目標,就賣出並止盈
            if profit >= self.params.take_profit:
                self.close()


##########################
# 主程序開始
#########################

# 建立大腦引擎對象
cerebro = bt.Cerebro()


# 建立行情資料對象,載入資料
# data = bt.feeds.PandasData(dataname=yf.download("MSFT", "2011-01-01", "2023-01-01"))
data = bt.feeds.PandasData(dataname=yf.download("2330.TW", "2011-01-01", "2023-01-01"))
# print(yf.download("MSFT", "2011-01-01", "2023-01-01"), type(yf.download("MSFT", "2011-01-01", "2023-01-01")))
# input()

# self.datas[0] 日K數據, self.datas[1] 月K數據
data = cerebro.resampledata(data, timeframe=bt.TimeFrame.Months, compression=1)
cerebro.adddata(data)  # 將行情資料對象注入引擎

cerebro.addstrategy(SmaCross)  # 將策略注入引擎

cerebro.broker.setcash(10000.0)  # 設定初始資金

# 加入pyfolio分析者
cerebro.addanalyzer(bt.analyzers.PyFolio, _name="pyfolio")

results = cerebro.run()  # 運行
strat = results[0]
pyfoliozer = strat.analyzers.getbyname("pyfolio")
returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items()
pf.create_full_tear_sheet(returns)

如何提高BACKTRADER回測性能1倍以上且最佳化記憶體

出處:https://www.itbook5.com/12372/

使用200萬條K線的資料,測試backtrader的回測性能如何?

為了做到這一點,第一件事就是產生的足夠的K線。所以,我們會做以下動作:

  • 產生100支股票
  • 每支股票 20000條K線資料

100個股票資料檔案總計200萬 根K線資料.

程式碼:

import numpy as np
import pandas as pd

COLUMNS = ['open', 'high', 'low', 'close', 'volume', 'openinterest']
CANDLES = 20000
STOCKS

dateindex = pd.date_range(start='2010-01-01', periods=CANDLES, freq='15min')

for i in range(STOCKS):

    data = np.random.randint(10, 20, size=(CANDLES, len(COLUMNS)))
    df = pd.DataFrame(data * 1.01, dateindex, columns=COLUMNS)
    df = df.rename_axis('datetime')
    df.to_csv('candles{:02d}.csv'.format(i))

這會生成 100 個檔案,從candles00.csv到``candles99.csv. 其中實際值並不重要。擁有標準 datetimeOHLCV(和OpenInterest)才是最重要的。

測試系統

  • 硬體/作業系統:將使用配備 Intel i7 和 32 GB 記憶體的Windows 10的 15.6″筆記型電腦。
  • Python : CPython3.6.1pypy3 6.0.0
  • 其他:持續運行並佔用大約 20% 的 CPU 的應用程式。正在運行著Chrome(102 個處理程序)、Edge、Word、Powerpoint、Excel 和一些小型應用程式等通常的程序。

默認組態

讓我們回顧一下backtrader的默認執行階段組態是什麼:

  • 如果可能,預載入所有資料饋送
  • 如果可以預載入所有資料饋送,則以批處理模式運行(命名為runonce
  • 首先預先計算所有指標
  • 逐步瞭解策略邏輯和經紀人

runonce在默認批處理模式下執行

我們的測試指令碼(完整原始碼見底部)將打開這 100 個檔案並使用backtrader默認的組態運行。

$ ./two-million-candles.py
Cerebro Start Time:          2019-10-26 08:33:15.563088
Strat Init Time:             2019-10-26 08:34:31.845349
Time Loading Data Feeds:     76.28
Number of data feeds:        100
Strat Start Time:            2019-10-26 08:34:31.864349
Pre-Next Start Time:         2019-10-26 08:34:32.670352
Time Calculating Indicators: 0.81
Next Start Time:             2019-10-26 08:34:32.671351
Strat warm-up period Time:   0.00
Time to Strat Next Logic:    77.11
End Time:                    2019-10-26 08:35:31.493349
Time in Strategy Next Logic: 58.82
Total Time in Strategy:      58.82
Total Time:                  135.93
Length of data feeds:        20000

記憶體使用:觀察到 348 MB 的峰值

大部分時間實際上都花在預載入資料(98.63秒)上,其餘時間花在策略上,包括在每次迭代中通過代理(73.63秒)。總時間為173.26秒。

根據您想要計算它的方式,性能是:

  • 考慮到整個執行階段間:14,713根K線/秒

說明以這樣的資料量backtrader處理起來,基本沒有壓力,記憶體的處理上,還可以通過參數的設定進行最佳化。將在後面做更多的探索。

比較使用pypy的方案

使用pypy的情況下,運行結果如下:

$ ./two-million-candles.py
Cerebro Start Time:          2019-10-26 08:39:42.958689
Strat Init Time:             2019-10-26 08:40:31.260691
Time Loading Data Feeds:     48.30
Number of data feeds:        100
Strat Start Time:            2019-10-26 08:40:31.338692
Pre-Next Start Time:         2019-10-26 08:40:31.612688
Time Calculating Indicators: 0.27
Next Start Time:             2019-10-26 08:40:31.612688
Strat warm-up period Time:   0.00
Time to Strat Next Logic:    48.65
End Time:                    2019-10-26 08:40:40.150689
Time in Strategy Next Logic: 8.54
Total Time in Strategy:      8.54
Total Time:                  57.19
Length of data feeds:        20000

總時間已經從 135.93秒減少到57.19秒。性能提高了一倍多

性能:34,971根K線/秒

記憶體使用:觀察到 269 MB 的峰值。

這也是對標準 CPython 直譯器的重要改進。

Handling 2M的蠟燭出核心memory

如果考慮到backtrader有多個用於執行回測會話的組態選項,所有這些都可以得到改進,包括最佳化緩衝區和僅使用所需的最少資料集(理想情況下僅使用 size 的緩衝區,這只會發生在理想場景)

class backtrader.Cerebro()
參數:

preload(默認True:)
是否預加載data feeds傳遞給 cerebro

runonce(默認:True)
以矢量化模式運行Indicators以加速整個系統。策略和觀察者將始終基於事件運行

live(默認:False)
默認是回測數據。

當使用實時數據時設置成True(或通過數據的islive 方法)

這將同時停用preload和runonce。它對內存節省方案沒有影響。

以矢量化模式運行Indicators以加速整個系統。策略和觀察者將始終基於事件運行

maxcpus(默認值:None -> 所有可用內核)
同時使用多少個內核進行優化

stdstats(默認:True)
默認將添加真正的默認觀察員:經紀人(現金和價值)、交易和買入賣出

oldbuysell(默認:False)(與畫圖相關)
如果stdstatsis:True 時觀察者自動添加,則此開關使用BuySell

False:其中買入/賣出信號分別繪製在低/高價下方/上方,以避免混亂

True:在該行為中繪製買入/賣出信號在給定時間的訂單執行的平均價格。這當然會在 OHLC 條的頂部或在 Close 的 Line 上,從而難以識別。

oldtrades(默認:False)(與畫圖相關)
如果stdstatsis:True時觀察者自動添加,則此開關控制Trades 

False:其中所有數據的交易都用不同的標記繪製

True:同一方向的交易用相同的標記繪製交易,僅區分它們是正數還是負數

exactbars(默認:False)
使用默認值,存儲在一行中的每個值都保存在內存中

`True` 或 `1`:所有“行”對象將內存使用量減少到自動計算的最小週期。

  如果簡單移動平均線的週期為 30,則基礎數據將始終具有 30 個柱的運行緩衝區,以允許計算簡單移動平均線

  * 此設置將停用 `preload` 和 `runonce` 

  * 使用此設置也會停用**繪圖** 

objcache (default: False)
如果為True實現line對象的緩存。

writer(默認: False)
如果設置為True時 它將標準信息的輸出生成一個默認文件

tradehistory(默認: False)
如果設置為True,它將在所有策略的每筆交易中激活更新事件記錄log。這也可以在每個策略的上使用set_tradehistory來實現

optdatas(默認:True)
如果True優化(並且preload和runonce也是True),數據預加載將在主進程中只進行一次,以節省時間和資源。

optreturn(默認:True)
如果True優化結果只有params屬性和analyzers指標,而不是完整Strategy 對象(以及所有數據、指標、觀察者……),這樣可以優化速度,測試顯示改善13% - 15%的執行時間

oldsync(默認False:)
從版本 1.9.0.99 開始,多個數據(相同或不同時間範圍)的同步已更改為允許不同長度的數據。

如果希望使用 data0 作為系統主控的舊行為,請將此參數設置為 true

tz(默認:None)
為策略添加全球時區。論據tz可以是

* `None`:在這種情況下,策略顯示的日期時間將採用UTC,這是標準行為

* `pytz` 實例。它將用於將 UTC 時間轉換為所選時區

* `string`。將嘗試實例化 `pytz` 實例。

* `整數`。
  對於策略,使用與 `self.datas` 迭代中相應的 `data`相同的時區(`0` 將使用來自 `data0` 的時區)

cheat_on_open(默認:False)
當為True時next_open調用發生在next方法調用之前。此時指標尚未重新計算。這允許發佈一個考慮前一天指標但使用open價格計算的訂單

對於 cheat_on_open 訂單執行,還需要調用cerebro.broker.set_coo(True)或實例化一個經紀人 BackBroker(coo=True)(其中coo代表 cheat-on-open)或將broker_coo參數設置為True. 除非在下面禁用,否則 Cerebro 會自動執行此操作。

broker_coo(默認:True)
這將自動調用set_coo代理的方法True來激活cheat_on_open執行。cheat_on_open要同時為True

quicknotify(默認:False)
經紀人通知在下一個價格交付之前交付 。對於回溯測試,這沒有任何影響,但是對於實時經紀人,可以在柱線交付之前很久就發出通知。設置為True通知將盡快發送(請參閱qcheck實時提要)

設置False為兼容性。可以改為True

要使用的選項是exactbars=True. 從文件中 exactbars(這是Cerebro在實例化或呼叫時給出的參數run

為了最大程度的最佳化並且停用繪圖,也將使用stdstats=False,停用現金、價值和交易的標準觀察者

$ ./two-million-candles.py --cerebro exactbars=False,stdstats=False
Cerebro Start Time:          2019-10-26 08:37:08.014348
Strat Init Time:             2019-10-26 08:38:21.850392
Time Loading Data Feeds:     73.84
Number of data feeds:        100
Strat Start Time:            2019-10-26 08:38:21.851394
Pre-Next Start Time:         2019-10-26 08:38:21.857393
Time Calculating Indicators: 0.01
Next Start Time:             2019-10-26 08:38:21.857393
Strat warm-up period Time:   0.00
Time to Strat Next Logic:    73.84
End Time:                    2019-10-26 08:39:02.334936
Time in Strategy Next Logic: 40.48
Total Time in Strategy:      40.48
Total Time:                  114.32
Length of data feeds:        20000

性能:17,494根K線/秒

記憶體使用:75M位元組(從開始回測開始到結束,穩定在這個數值)

讓我們與之前的非最佳化運行進行比較

  • 無需花費76秒鐘預載入資料,而是立即開始回測。
  • 總時間是114.32秒 比 135.93秒改進15.90%
  • 使用記憶體改進了68.5%

再次pypy

既然我們知道如何最佳化,讓我們照著做一次pypy

$ ./two-million-candles.py --cerebro exactbars=True,stdstats=False 
Cerebro Start Time: 2019-10-26 08:44:32.309689 
Strat Init Time: 2019-10-26 08:44:32.406689
時間加載數據饋送:0.10
數據饋送數量:100 
Strat 開始時間:2019-10-26 08:44:32.409689 
Pre-Next Start Time:2019-10-26 08:44:32.451689
時間計算指標:0.04 
Next Start Time:2019 -10-26 08:44:32.451689 戰略
預熱期時間:0.00戰略下一個邏輯時間
:0.14
結束時間:2019-10-26 08:45:38.918693
戰略下一個邏輯時間:66.47
戰略總時間:66.47
總時間:66.61
數據饋送長度:20000

性能:30,025根K線/秒

記憶體使用:恆定在49 M位元組

將其與之前運行進行比較:

  • 66.61秒 比114.32t秒,在執行階段間上有``41.73%的改進。
  • 49 M位元組比``75 M位元組,在記憶體上有34.6%的改進。

在這種情況下,與批處理模式pypy相比,它無法擊敗自己的時間。這是意料之中的,因為在預載入時,計算器指示是在向量化模式下完成的。

無論如何,它仍然做得非常好,並且記憶體消耗有了重要的改善

完整的交易運行

該指令碼可以建立指標(移動平均線)並使用移動平均線的交叉短期/長期策略對 100 個股票執行回測*。*讓我們用pypy來做,並且知道使用批處理模式會更好,就這樣吧。

$ ./two-million-candles.py --strat indicators=True,trade=True
Cerebro Start Time:          2019-10-26 08:57:36.114415
Strat Init Time:             2019-10-26 08:58:25.569448
Time Loading Data Feeds:     49.46
Number of data feeds:        100
Total indicators:            300
Moving Average to be used:   SMA
Indicators period 1:         10
Indicators period 2:         50
Strat Start Time:            2019-10-26 08:58:26.230445
Pre-Next Start Time:         2019-10-26 08:58:40.850447
Time Calculating Indicators: 14.62
Next Start Time:             2019-10-26 08:58:41.005446
Strat warm-up period Time:   0.15
Time to Strat Next Logic:    64.89
End Time:                    2019-10-26 09:00:13.057955
Time in Strategy Next Logic: 92.05
Total Time in Strategy:      92.21
Total Time:                  156.94
Length of data feeds:        20000

性能:12,743根K線/秒

記憶體使用1300 M位元組觀察到一個峰值。

由於增加了指標和交易,執行時間明顯增加了,但是為什麼記憶體使用也增加了?

在得出任何結論之前,讓我們嘗試建立指標但不進行交易

$ ./two-million-candles.py --strat indicators=True
Cerebro Start Time:          2019-10-26 09:05:55.967969
Strat Init Time:             2019-10-26 09:06:44.072969
Time Loading Data Feeds:     48.10
Number of data feeds:        100
Total indicators:            300
Moving Average to be used:   SMA
Indicators period 1:         10
Indicators period 2:         50
Strat Start Time:            2019-10-26 09:06:44.779971
Pre-Next Start Time:         2019-10-26 09:06:59.208969
Time Calculating Indicators: 14.43
Next Start Time:             2019-10-26 09:06:59.360969
Strat warm-up period Time:   0.15
Time to Strat Next Logic:    63.39
End Time:                    2019-10-26 09:07:09.151838
Time in Strategy Next Logic: 9.79
Total Time in Strategy:      9.94
Total Time:                  73.18
Length of data feeds:        20000

性能:27,329 根K線/秒

記憶體使用:(600 M位元組在最佳化exactbars模式下做同樣的事情只會消耗60 M位元組,但會增加執行時間,因為 pypy它本身不能最佳化這麼多)

有了交易,記憶體使用量確實增加了。原因是對像是由代理建立、傳遞和保存的Order和``Trade。

還有該資料集包含隨機值,其產生數量龐大交叉的,因此有大量的訂單和交易。對於常規資料集,不會有類似的行為。

結論

    1. backtrader可以使用默認組態輕鬆處理2M蠟燭圖(預載入記憶體資料)
    2. backtrader可以在非預載入最佳化模式下運行,將緩衝區減少到最小,以進行減少記憶體使用進行回測
    3. 最佳化的非預載入模式下進行回測時,記憶體消耗的增加來自於代理產生的管理開銷。
    4. 即使交易、使用指標和經紀人不斷阻礙,表現也是12,473根K線/秒
    5. 儘可能使用pypy(如果您不需要繪圖的時候)

測試指令碼

這裡是原始碼

#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
import argparse
import datetime

import backtrader as bt


class St(bt.Strategy):
    params = dict(
        indicators=False,
        indperiod1=10,
        indperiod2=50,
        indicator=bt.ind.SMA,
        trade=False,
    )

    def __init__(self):
        self.dtinit = datetime.datetime.now()
        print('Strat Init Time:             {}'.format(self.dtinit))
        loaddata = (self.dtinit - self.env.dtcerebro).total_seconds()
        print('Time Loading Data Feeds:     {:.2f}'.format(loaddata))

        print('Number of data feeds:        {}'.format(len(self.datas)))
        if self.p.indicators:
            total_ind = self.p.indicators * 3 * len(self.datas)
            print('Total indicators:            {}'.format(total_ind))
            indname = self.p.indicator.__name__
            print('Moving Average to be used:   {}'.format(indname))
            print('Indicators period 1:         {}'.format(self.p.indperiod1))
            print('Indicators period 2:         {}'.format(self.p.indperiod2))

            self.macross = {}
            for d in self.datas:
                ma1 = self.p.indicator(d, period=self.p.indperiod1)
                ma2 = self.p.indicator(d, period=self.p.indperiod2)
                self.macross[d] = bt.ind.CrossOver(ma1, ma2)

    def start(self):
        self.dtstart = datetime.datetime.now()
        print('Strat Start Time:            {}'.format(self.dtstart))

    def prenext(self):
        if len(self.data0) == 1:  # only 1st time
            self.dtprenext = datetime.datetime.now()
            print('Pre-Next Start Time:         {}'.format(self.dtprenext))
            indcalc = (self.dtprenext - self.dtstart).total_seconds()
            print('Time Calculating Indicators: {:.2f}'.format(indcalc))

    def nextstart(self):
        if len(self.data0) == 1:  # there was no prenext
            self.dtprenext = datetime.datetime.now()
            print('Pre-Next Start Time:         {}'.format(self.dtprenext))
            indcalc = (self.dtprenext - self.dtstart).total_seconds()
            print('Time Calculating Indicators: {:.2f}'.format(indcalc))

        self.dtnextstart = datetime.datetime.now()
        print('Next Start Time:             {}'.format(self.dtnextstart))
        warmup = (self.dtnextstart - self.dtprenext).total_seconds()
        print('Strat warm-up period Time:   {:.2f}'.format(warmup))
        nextstart = (self.dtnextstart - self.env.dtcerebro).total_seconds()
        print('Time to Strat Next Logic:    {:.2f}'.format(nextstart))
        self.next()

    def next(self):
        if not self.p.trade:
            return

        for d, macross in self.macross.items():
            if macross > 0:
                self.order_target_size(data=d, target=1)
            elif macross < 0:
                self.order_target_size(data=d, target=-1)

    def stop(self):
        dtstop = datetime.datetime.now()
        print('End Time:                    {}'.format(dtstop))
        nexttime = (dtstop - self.dtnextstart).total_seconds()
        print('Time in Strategy Next Logic: {:.2f}'.format(nexttime))
        strattime = (dtstop - self.dtprenext).total_seconds()
        print('Total Time in Strategy:      {:.2f}'.format(strattime))
        totaltime = (dtstop - self.env.dtcerebro).total_seconds()
        print('Total Time:                  {:.2f}'.format(totaltime))
        print('Length of data feeds:        {}'.format(len(self.data)))


def run(args=None):
    args = parse_args(args)

    cerebro = bt.Cerebro()

    datakwargs = dict(timeframe=bt.TimeFrame.Minutes, compression=15)
    for i in range(args.numfiles):
        dataname = 'candles{:02d}.csv'.format(i)
        data = bt.feeds.GenericCSVData(dataname=dataname, **datakwargs)
        cerebro.adddata(data)

    cerebro.addstrategy(St, **eval('dict(' + args.strat + ')'))
    cerebro.dtcerebro = dt0 = datetime.datetime.now()
    print('Cerebro Start Time:          {}'.format(dt0))
    cerebro.run(**eval('dict(' + args.cerebro + ')'))


def parse_args(pargs=None):
    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description=(
            'Backtrader Basic Script'
        )
    )

    parser.add_argument('--numfiles', required=False, default=100, type=int,
                        help='Number of files to rea')

    parser.add_argument('--cerebro', required=False, default='',
                        metavar='kwargs', help='kwargs in key=value format')

    parser.add_argument('--strat', '--strategy', required=False, default='',
                        metavar='kwargs', help='kwargs in key=value format')


    return parser.parse_args(pargs)


if __name__ == '__main__':
    run()

使用python連線kafka介紹

整體架構調整如下圖:

img

歡看文字描述的朋友可以看看如下解釋

在叢集外的一臺機器上啟動一個python指令碼,該指令碼從資料庫讀出資料,然後將這些資料分發給多個執行緒,每個執行緒各自訪問url,解析正文,建立一個kafka的生產者往kafka中傳送資料(有多少條執行緒,就有多少個kafka資訊),接著在內網環境中,啟動一個kafka消費者,將所有生產者傳遞過來的資料解析,並批量插入資料庫。

然後我再說幾個點:

1、我試過,如果每個kafka生產者獲取到個url的正文,就往kafka傳送訊息,這樣相對來說可以更加保證資料不丟失,但是消費者會消費來不及,也就是說會有資料延遲,而且把生產者關了一段時間,消費者還消費不完(延遲挺嚴重),所以還是得通過拼接字串的方式,使得一條kafka訊息中,有幾十(甚至可以更多)條資料,然後消費端再解析這個資料,批量插入資料庫(詳細看程式碼),節省時間和資源。

2、資料是否會出現問題,會的,我來列舉下會丟或者重複資料的幾個地方。

  • 丟失資料:

    • urllib獲取頁面資訊的時候,有可能超時,所以那個頁面就跳過去了
    • 生產者傳送資料的時候,有可能因為網路或者其他原因沒傳送成功(大部分是因為網路)
  • 資料重複:

    • 消費者消費完資料之後,由於消費者的程式掛了,沒能將偏移量提交到zookeeper上,導致下次再開啟消費者的時候,重複消費資料,但是因為我的消費者壓力不大,所以沒有出現主動掛掉的情況

總結:我會通過sql的join方式,每次都從資料庫中拿出那些未被處理的資料,也就是說丟資料也無所謂,資料重複的問題,可以用主鍵(一張表內不會出現兩條相同主鍵的資料),或者最後用sql來對錶進行去重的方式來解決。

3、python連線kafka有兩個包:pykafka和kafka-python

具體他們之間孰好孰壞,可以看下pykafka開發者在自己的GitHub給別人的回答:https://github.com/Parsely/pykafka/issues/334,當然是全英文的。不過(作者:高爾夫golf,標明轉載)https://blog.csdn.net/konglongaa/article/details/81206889有對於這個issue的中文翻譯和總結,如果大家有興趣可以去看下,當然,這個issue的回答有些年代了,所以也不一定完全正確,比如裡面說pykafka只支援0.8.2版本的kafka,但是我使用了kafka_2.11-1.0.1版本,完全沒有版本不相容的問題。

程式碼如下:

生產者:

from concurrent.futures import ThreadPoolExecutor
import time
import urllib.request
import  psycopg2
from lxml import etree
from pykafka import KafkaClient


def insertManyRow(strings):
    print('insertManyRow:',strings)
    # 多條資料間使用 =.= 來做分隔符
    b="=.=".join(strings)
    try:
        print('進入到生產者程式碼!')

        client = KafkaClient(hosts="broker1:埠1,broker2:埠2,broker3:埠3")
        # 檢視所有topic
        print(client.topics)
        topic = client.topics['指定傳送的topic名稱']  # 選擇一個topic
		# 同步傳送資料
        with topic.get_sync_producer() as producer:
			# 資料轉換成byte才可以傳送
            producer.produce(bytes(b, encoding="utf8"))

    except Exception as e:
        print("傳送失敗%s" % (e))
      

def productList(rows):
    string=''
    # 將多條資料放入list中
    strings=[]
    count = 0
    for row in rows:
        file = urllib.request.urlopen(row[2],timeout=5)

        try: 
            data = file.read()
            #是否被封號,從偏移量3000的位置往下找
            isBan=str(data).find('被封號的字串', 3000)
            if(isBan!=-1):
                string='ip被封'
            else:
                selector = etree.HTML(data)
                data = selector.xpath('//*[@id="zhengwen"]/p/span/text()')
                # 將獲取到的多個正文內容拼接成一條字串
                for i in data:
                    if (i != None):
                        string = string + i  

            # 列印檢視
            print('正文:', string)
            # 將資料庫中一條資料的多個欄位通過 -.- 拼接到一起
            content=row[0]+'-.-'+row[1]+'-.-'+string+'-.-'+row[3]+'-.-'+row[4]
            # 放入list中
            strings.append(content)
            # 清空字串
            string = ''
            print("集合:", strings)
            print("集合長度:",len(strings))
            count = count + 1
            # 每十條資料就呼叫一次kafka生產者的程式碼
            if (count >= 10):
                print('進入到insertManyRow')
                insertManyRow(strings)
                strings = []
                count = 0
        except Exception as e:
            print("執行緒出錯:%s" % (e))

if __name__ == '__main__':
    conn = psycopg2.connect(database="資料庫", user="使用者名稱", password="密碼",
                                   host="ip",
                                   port="埠")
    cur = conn.cursor()
    
    sql = "查詢的sql,查出未處理的url"
    cur.execute(sql1)
    rows = cur.fetchall()
    print('拉取到資料')
    start=time.time()
    # 開啟10個執行緒,每個執行緒每次拉取10條url
    with ThreadPoolExecutor(10) as executor:
        for i in range(0, len(rows)//10, 1):
            executor.submit(productList, rows[i*10:(i+1)*10])
    end = time.time()
    print("time: " + str(end - start))
    conn.close()

消費者:

from pykafka import KafkaClient
import psycopg2

client = KafkaClient(hosts="broker1:埠1,broker2:埠2,broker3:埠3")
# 檢視所有topic
print(client.topics)
topic = client.topics['指定傳送的topic名稱']  # 選擇一個topic

#獲得一個均衡的消費者
balanced_consumer = topic.get_balanced_consumer(
consumer_group=bytes('消費者組名',encoding='utf-8'),
auto_commit_enable=True,# 設定為False的時候不需要新增consumer_group,直接連線topic即可取到訊息
#kafka在zk上的路徑,這個路徑應該和kafka的broker配置的zk路徑一樣(不然zk上會放得亂七八糟的。。。)
zookeeper_connect='zk1:埠1,zk2:埠1,zk3:埠3/kafka在zk上的路徑'
)


# arrs=[]
insertarr=[]

for message in balanced_consumer:
    print(message)
    if message is not None:
        #print(message.offset, message.value, type(message.value), str(message.value, encoding="utf8"))
		#將接受到的資料轉換成executemany能接受的資料格式
        arrs=str(message.value, encoding="utf8").split('=.=')
        for arr in arrs:
            a=arr.split('-.-')
            insertarr.append(a)
    try:
		conn = psycopg2.connect(database="資料庫", user="使用者名稱", password="密碼",
                                   host="ip",
                                   port="埠")
		cur = conn.cursor()
        sql = "INSERT INTO 資料庫.表名(欄位1,欄位2,欄位3,欄位4,欄位5) VALUES(%s,%s,%s,%s,%s)"
        print(insertarr)
        cur.executemany(sql, insertarr)
        conn.commit()
        insertarr = []
        conn.close()
    except Exception as e:
        print("插入錯誤:%s" % (e))
        insertarr=[]
        conn.close()

所以,到這裡,這個爬蟲系列的文章就更新到這裡了!因為我使用到了kafka框架,下一篇文章,應該會說下kafka生產者,消費者的一些東西,比如,我使用的get_balanced_consumer這個api,然後還有第一篇文章說的分類的東西,我也會開部落格來記錄!

Python操作Kafka的通俗總結

kafka-python文檔:KafkaConsumer - kafka-python 2.0.2-dev documentation

一、基本概念

  • Topic:一組消息數據的標記符;
  • Producer:生產者,用於生產數據,可將生產後的消息送入指定的Topic;
  • Consumer:消費者,獲取數據,可消費指定的Topic;
  • Group:消費者組,同一個group可以有多個消費者,一條消息在一個group中,只會被一個消費者獲取;
  • Partition:分區,為了保證kafka的吞吐量,一個Topic可以設置多個分區。同一分區只能被一個消費者訂閱。

二、本地安裝與啟動(基於Docker)

  1. 下載zookeeper鏡像與kafka鏡像:
docker pull wurstmeister/zookeeper
docker pull wurstmeister/kafka
  1. 本地啟動zookeeper
docker run -d --name zookeeper -p 2181:2181 -t wurstmeister/zookeeper  
  1. 本地啟動kafka
docker run -d --name kafka --publish 9092:9092 --link zookeeper \
--env KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 \
--env KAFKA_ADVERTISED_HOST_NAME=localhost \
--env KAFKA_ADVERTISED_PORT=9092 \
wurstmeister/kafka:latest 

注意:上述代碼,將kafka啟動在9092端口

  1. 進入kafka bash
docker exec -it kafka bash
cd /opt/kafka/bin
  1. 創建Topic,分區為2,Topic name為'kafkademo'
kafka-topics.sh --create --zookeeper zookeeper:2181 \
--replication-factor 1 --partitions 2 --topic kafkademo
  1. 查看當前所有topic
kafka-topics.sh --zookeeper zookeeper:2181 --list
  1. 安裝kafka-python
pip install kafka-python

三、生產者(Producer)與消費者(Consumer)

生產者和消費者的簡易Demo,這裡一起演示:

from kafka import KafkaConsumer
import json


def consumer_demo():
    consumer = KafkaConsumer(
        "kafkademo",
        bootstrap_servers=["localhost:9092"],
        group_id="test",
        # api_version='2.0.2'
        api_version=(0, 10),
    )

    print(consumer.bootstrap_connected())
    print(consumer.topics())
    for message in consumer:
        print(consumer.bootstrap_connected())
        print(
            "receive, key: {}, value: {}".format(
                json.loads(message.key.decode()), json.loads(message.value.decode())
            )
        )


if __name__ == "__main__":
    consumer_demo()
from kafka import KafkaProducer
from kafka.errors import kafka_errors
import traceback
import json


def producer_demo():
    # 假設生產的消息為鍵值對(不是一定要鍵值對),且序列化方式為json
    producer = KafkaProducer(
        bootstrap_servers=["localhost:9092"],
        key_serializer=lambda k: json.dumps(k).encode(),
        value_serializer=lambda v: json.dumps(v).encode(),
    )
    # 發送三條消息
    for i in range(0, 3):
        future = producer.send(
            "kafkademo", key="count_num", value=str(i), partition=1  # 同一個key值,會被送至同一個分區
        )  # 向分區1發送消息
        print("send {}".format(str(i)))
        try:
            future.get(timeout=10)  # 監控是否發送成功
        except kafka_errors:  # 發送失敗拋出kafka_errors
            traceback.format_exc()


if __name__ == "__main__":
    producer_demo()

這裡建議起兩個terminal,或者兩個jupyter notebook頁面來驗證。

先執行消費者:

consumer_demo()

再執行生產者:

producer_demo()

會看到如下輸出:

>>> producer_demo()
send 0
send 1
send 2
>>> consumer_demo()
receive, key: count_num, value: 0
receive, key: count_num, value: 1
receive, key: count_num, value: 2

四、消費者進階操作

(1)初始化參數:

列舉一些KafkaConsumer初始化時的重要參數:

  • group_id

高並發量,則需要有多個消費者協作,消費進度,則由group_id統一。例如消費者A與消費者B,在初始化時使用同一個group_id。在進行消費時,一條消息被消費者A消費後,在kafka中會被標記,這條消息不會再被B消費(前提是A消費後正確commit)。

  • key_deserializer, value_deserializer

與生產者中的參數一致,自動解析。

  • auto_offset_reset

消費者啟動的時刻,消息隊列中或許已經有堆積的未消費消息,有時候需求是從上一次未消費的位置開始讀(則該參數設置為earliest),有時候的需求為從當前時刻開始讀之後產生的,之前產生的數據不再消費(則該參數設置為latest)。

  • enable_auto_commit, auto_commit_interval_ms

是否自動commit,當前消費者消費完該數據後,需要commit,才可以將消費完的信息傳回消息隊列的控制中心。enable_auto_commit設置為True後,消費者將自動commit,並且兩次commit的時間間隔為auto_commit_interval_ms。

(2)手動commit

def consumer_demo():
    consumer = KafkaConsumer(
        'kafkademo', 
        bootstrap_servers=':9092',
        group_id='test',
        enable_auto_commit=False
    )
    for message in consumer:
        print("receive, key: {}, value: {}".format(
            json.loads(message.key.decode()),
            json.loads(message.value.decode())
            )
        )
        consumer.commit()

(3)查看kafka堆積剩餘量

在線環境中,需要保證消費者的消費速度大於生產者的生產速度,所以需要檢測kafka中的剩餘堆積量是在增加還是減小。可以用如下代碼,觀測隊列消息剩餘量:

consumer = KafkaConsumer(topic, **kwargs)
partitions = [TopicPartition(topic, p) for p in consumer.partitions_for_topic(topic)]

print("start to cal offset:")

# total
toff = consumer.end_offsets(partitions)
toff = [(key.partition, toff[key]) for key in toff.keys()]
toff.sort()
print("total offset: {}".format(str(toff)))
    
# current
coff = [(x.partition, consumer.committed(x)) for x in partitions]
coff.sort()
print("current offset: {}".format(str(coff)))

# cal sum and left
toff_sum = sum([x[1] for x in toff])
cur_sum = sum([x[1] for x in coff if x[1] is not None])
left_sum = toff_sum - cur_sum
print("kafka left: {}".format(left_sum))

Check if Kafka Broker is up and running in Python

Using confluent-kafka-python and AdminClient

https://stackoverflow.com/questions/61226910/how-to-programmatically-check-if-kafka-broker-is-up-and-running-in-python

# Example using confuent_kafka
from confluent_kafka.admin import AdminClient

kafka_broker = {'bootstrap.servers': 'localhost:9092'}
admin_client = AdminClient(kafka_broker)
topics = admin_client.list_topics().topics

if not topics: 
    raise RuntimeError()

Using kafka-python and KafkaConsumer

# example using kafka-python
import kafka


consumer = kafka.KafkaConsumer(group_id='test', bootstrap_servers=['localhost:9092'])
topics = consumer.topics()

if not topics: 
    raise RuntimeError()

安裝 Redis

sudo apt install redis-server
sudo systemctl status redis-server
import redis

def delete_all_keys():
    redis_conn = redis.Redis(host="127.0.0.1", port=6379, db=8)
    redis_conn.flushdb()

if __name__ == "__main__":
    delete_all_keys()
import redis
r = redis.StrictRedis(host='localhost', port=6379, db=0)
r.set('foo', 'bar')
print(r.get('foo'))

備份&還原

from github import Github
from finlab import data
from os.path import getsize
import finlab
import pickle
import redis
import zlib
import time


def update_github(git_file):
    token = ""
    # Step 1: Create a Github instance:
    g = Github(token)
    repo = g.get_user().get_repo("stockinfo")

    all_files = []
    contents = repo.get_contents("")
    while contents:
        file_content = contents.pop(0)
        if file_content.type == "dir":
            contents.extend(repo.get_contents(file_content.path))
        else:
            file = file_content
            all_files.append(
                str(file).replace('ContentFile(path="', "").replace('")', "")
            )

    print(all_files)
    with open(git_file, "rb") as file:
        content = file.read()

    if git_file in all_files:
        contents = repo.get_contents("/")
        for item in contents:
            if item.path == git_file:
                contents = item
                break
        print(contents)

        while True:
            try:
                repo.update_file(
                    contents.path, "committing files", content, contents.sha
                )
                break
            except Exception as e:
                print(e)
                time.sleep(10)
                continue

        print(git_file + " UPDATED")
    else:
        while True:
            try:
                repo.create_file(git_file, "committing files", content)
                break
            except Exception as e:
                print(e)
                time.sleep(10)
                continue
        print(git_file + " CREATED")


def dump():
    redis_conn = redis.Redis(host="127.0.0.1", port=6379, db=8)
    data_keys = redis_conn.keys()
    for key in data_keys:
        with open(f'{key.decode("utf-8")}.bin', "wb") as f:
            f.write(redis_conn.get(key))
        print(
            key.decode("utf-8"),
            "==========================================================================================",
        )
        # github 單一檔案無法上傳超過50mb
        if getsize(f'{key.decode("utf-8")}.bin') < 52428800:
            update_github(f'{key.decode("utf-8")}.bin')
    redis_conn.close()


def restore(dataset):
    redis_conn = redis.Redis(host="127.0.0.1", port=6379, db=9)
    with open(f"{dataset}.bin", "rb") as f:
        redis_conn.set(dataset.encode("utf-8"), f.read())
    redis_conn.close()


def get_data():
    data_list = [
        "benchmark_return:發行量加權股價報酬指數",
        "etl:adj_close",
        "etl:finlab_tw_stock_market_ind",
        "financial_statement:合約負債_流動",
        "financial_statement:投資活動之淨現金流入_流出",
        "financial_statement:營業收入淨額",
        "financial_statement:營業活動之淨現金流入_流出",
        "financial_statement:研究發展費",
        "financial_statement:籌資活動之淨現金流入_流出",
        "financial_statement:股本",
        "financial_statement:股東權益總額",
        "fundamental_features:ROE稅後",
        "fundamental_features:業外收支營收率",
        "fundamental_features:營收成長率",
        "fundamental_features:營業利益成長率",
        "fundamental_features:營業利益率",
        "fundamental_features:經常稅後淨利",
        "institutional_investors_trading_summary:投信買賣超股數",
        "inventory",
        "margin_balance:融資券總餘額",
        "margin_transactions:融資今日餘額",
        "margin_transactions:融資使用率",
        "monthly_revenue:上月比較增減(%)",
        "monthly_revenue:去年同月增減(%)",
        "monthly_revenue:當月營收",
        "par_value_change_otc:otc_par_value_change_divide_ratio",
        "par_value_change_tse:twse_par_value_change_divide_ratio",
        "price_earning_ratio:本益比",
        "price_earning_ratio:殖利率(%)",
        "price_earning_ratio:股價淨值比",
        "price:成交股數",
        "price:收盤價",
        "price:收盤價",
        "security_industry_themes",
        "tw_business_indicators:景氣對策信號(分)",
        "tw_total_nmi:臺灣非製造業NMI",
        "tw_total_pmi:未來六個月展望",
        "tw_total_pmi:製造業PMI",
    ]
    for dataset in data_list:
        data.get(dataset)


if __name__ == "__main__":
    finlab.login(
        ""
    )
    get_data()
    dump()
    redis_conn = redis.Redis(host="127.0.0.1", port=6379, db=8)
    data_keys = redis_conn.keys()
    redis_conn.close()

    redis_conn = redis.Redis(host="127.0.0.1", port=6379, db=9)
    for key in data_keys:
        k = key.decode("utf-8")
        restore(k)
        print(k)
        print(pickle.loads(zlib.decompress(redis_conn.get(k))))
    redis_conn.close()

以字典形式寫入數據到 Redis

import redis

# 建立 Redis 連線
r = redis.Redis(host="localhost", port=6379, db=0)

if True:
    # 使用多次 hset 方法設置多個鍵值對
    r.hset("my_dict", "key1", "value1")
    r.hset("my_dict", "key2", "value2")
else:
    # 使用字典一次性設置多個鍵值對
    data = {"key1": "value1", "key2": "value2"}
    r.hmset("my_dict", data)  # 此行仍可以正常運作,但會出現 DeprecationWarning 警告


# 從 Redis 讀取字典
my_dict = r.hgetall("my_dict")
my_dict = {key.decode("utf-8"): val.decode("utf-8") for key, val in my_dict.items()}
print(my_dict)

Redis Hash操作]

Redis 數據庫hash數據類型是一個string類型的key和value的映射表,適用於存儲對象。Redis 中每個 hash 可以存儲 232 - 1 鍵值對(40多億)。 hash表現形式上有些像pyhton中的dict,可以存儲一組關聯性較強的數據 , redis中Hash在內存中的存儲格式如下圖:

img

# 連接redis
 
import redis
 
host = '172.16.200.49'
port = 6379
 
pool = redis.ConnectionPool(host=host, port=port)
 
r = redis.Redis(connection_pool=pool)

hset(name, key, value)

# name對應的hash中設置一個鍵值對(不存在,則創建;否則,修改)
  
# 參數:
    # name,redis的name
    # key,name對應的hash中的key
    # value,name對應的hash中的value
  
# 註:
    # hsetnx(name, key, value),當name對應的hash中不存在當前key時則創建(相當於添加)
 
 
r.hset('p_info', 'name', 'bigberg')
r.hset('p_info', 'age', '22')
r.hset('p_info', 'gender', 'M')
 
# 設置了姓名、年齡和性別

hmset(name, mapping)

# 在name對應的hash中批量設置鍵值對
  
# 參數:
    # name,redis的name
    # mapping,字典,如:{'k1':'v1', 'k2': 'v2'}
  
# 如:
    # r.hmset('xx', {'k1':'v1', 'k2': 'v2'})
 
 
r.hmset('info_2', {'name': 'Jerry', 'species': 'mouse'})

hget(name, key)

# 在name對應的hash中獲取根據key獲取value
 
# 獲取的bytes 類型
print(r.hget('p_info', 'name').decode())
 
# 輸出
bigberg

hmget(name, key, *args)

# 在name對應的hash中獲取多個key的值
  
# 參數:
    # name,reids對應的name
    # keys,要獲取key集合,如:['k1', 'k2', 'k3']
    # *args,要獲取的key,如:k1,k2,k3
  
# 如:
print(r.hmget('p_info', ['name', 'age', 'gender']))

hgetall(name)

獲取name對應hash的所有鍵值
 
print(r.hgetall('p_info'))
 
#輸出是一個字典
 
{b'name': b'bigberg', b'gender': b'M', b'age': b'22'}

hlen(name)

# 獲取name對應的hash中鍵值對的個數
 
print(r.hlen('p_info'))
 
#輸出
3

hkeys(name)

# 獲取name對應的hash中所有的key的值
 
print(r.hkeys('p_info'))
 
#輸出
[b'name', b'age', b'gender']

hvals(name)

# 獲取name對應的hash中所有的value的值
 
print(r.hvals('p_info'))
 
#輸出
 [b'bigberg', b'22', b'M']

hexists(name, key)

# 檢查name對應的hash是否存在當前傳入的key
 
print(r.hexists('p_info', 'name'))
print(r.hexists('p_info', 'job'))
 
#輸出
True
False

hdel(name,*keys)

# 將name對應的hash中指定key的鍵值對刪除
 
r.hdel('p_info', 'gender')
print(r.hgetall('p_info'))
 
# 刪除了性別
#輸出
{b'name': b'bigberg', b'age': b'22'

hincrby(name, key, amount=1)

# 自增name對應的hash中的指定key的值,不存在則創建key=amount
# 參數:
    # name,redis中的name
    # key, hash對應的key
    # amount,自增數(整數)
 
r.hincrby('p_info', 'age', 1)
print(r.hget('p_info', 'age'))
 
#輸出,年齡增加1
b'23'

hincrbyfloat(name, key, amount=1.0)

# 自增name對應的hash中的指定key的值,不存在則創建key=amount
  
# 參數:
    # name,redis中的name
    # key, hash對應的key
    # amount,自增數(浮點數)
  
# 自增name對應的hash中的指定key的值,不存在則創建key=amount

hscan(name, cursor=0, match=None, count=None)

# 增量式迭代獲取,對於數據大的數據非常有用,hscan可以實現分片的獲取數據,並非一次性將數據全部獲取完,從而放置內存被撐爆
  
# 參數:
    # name,redis的name
    # cursor,遊標(基於遊標分批取獲取數據)
    # match,匹配指定key,默認None 表示所有的key
    # count,每次分片最少獲取個數,默認None表示採用Redis的默認分片個數
 
 
print(r.hscan('p_info', cursor=0))
print(r.hscan('p_info', cursor=0, match='n*'))
 
#輸出
 
(0, {b'age': b'23', b'address': b'hz', b'name': b'bigberg'})
(0, {b'name': b'bigberg'})

hscan_iter(name, match=None, count=None)

# 利用yield封裝hscan創建生成器,實現分批去redis中獲取數據
   
# 參數:
    # match,匹配指定key,默認None 表示所有的key
    # count,每次分片最少獲取個數,默認None表示採用Redis的默認分片個數
   
# 如:
    # for item in r.hscan_iter('xx'):
    #     print(item)

clickhouse

https://phoenixnap.com/kb/install-clickhouse-on-ubuntu-20-04

Installing ClickHouse on Ubuntu 20.04

sudo apt-get install -y apt-transport-https ca-certificates dirmngr
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 8919F6BD2B48D754

echo "deb https://packages.clickhouse.com/deb stable main" | sudo tee \
    /etc/apt/sources.list.d/clickhouse.list
    
sudo apt-get update

sudo apt-get install -y clickhouse-server clickhouse-client

sudo service clickhouse-server start
clickhouse-client # or "clickhouse-client --password" if you've set up a password.

Getting Started With ClickHouse

sudo systemctl start clickhouse-server
sudo systemctl status clickhouse-server
● clickhouse-server.service - ClickHouse Server (analytic DBMS for big data)
     Loaded: loaded (/lib/systemd/system/clickhouse-server.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2022-09-12 16:07:10 CST; 5s ago
   Main PID: 429578 (clckhouse-watch)
      Tasks: 205 (limit: 18726)
     Memory: 73.7M
     CGroup: /system.slice/clickhouse-server.service
             ├─429578 clickhouse-watchdog        --config=/etc/clickhouse-server/config.xml --pid-file=/run/clickhouse-server/clickhouse-server.pid
             └─429596 /usr/bin/clickhouse-server --config=/etc/clickhouse-server/config.xml --pid-file=/run/clickhouse-server/clickhouse-server.pid

 9月 12 16:07:10 nb-jasonyao systemd[1]: Started ClickHouse Server (analytic DBMS for big data).
 9月 12 16:07:10 nb-jasonyao clickhouse-server[429578]: Processing configuration file '/etc/clickhouse-server/config.xml'.
 9月 12 16:07:10 nb-jasonyao clickhouse-server[429578]: Logging trace to /var/log/clickhouse-server/clickhouse-server.log
 9月 12 16:07:10 nb-jasonyao clickhouse-server[429578]: Logging errors to /var/log/clickhouse-server/clickhouse-server.err.log
 9月 12 16:07:10 nb-jasonyao clickhouse-server[429596]: Processing configuration file '/etc/clickhouse-server/config.xml'.
 9月 12 16:07:10 nb-jasonyao clickhouse-server[429596]: Saved preprocessed configuration to '/var/lib/clickhouse/preprocessed_configs/config.xml'.
 9月 12 16:07:10 nb-jasonyao clickhouse-server[429596]: Processing configuration file '/etc/clickhouse-server/users.xml'.
 9月 12 16:07:10 nb-jasonyao clickhouse-server[429596]: Merging configuration file '/etc/clickhouse-server/users.d/default-password.xml'.
 9月 12 16:07:10 nb-jasonyao clickhouse-server[429596]: Saved preprocessed configuration to '/var/lib/clickhouse/preprocessed_configs/users.xml'.
clickhouse-client --password f0409 --user default

python clickhouse-driver

pip install clickhouse-driver
from clickhouse_driver import Client
from io import StringIO
import requests
import pandas as pd

client = Client(host="localhost", port="9000", user="default", password="f0409")

def get_wespai_data(url):
    headers = {
        "user-agent": "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36"
    }
    TAIGU1 = requests.get(url, headers=headers)
    TAIGU1.encoding = "utf-8"
    raw_data = pd.read_html(StringIO(TAIGU1.text))[0]
    #raw_data.to_csv(FILE_name, header=True, index=False, encoding="utf-8")
    return raw_data

df = get_wespai_data("https://stock.wespai.com/p/48812")


def read_sql(sql):
    data, columns = client.execute(sql, columnar=True, with_column_types=True)
    df = pd.DataFrame({re.sub(r"\W", "_", col[0]): d for d, col in zip(data, columns)})
    return df


def get_type_dict(tb_name):
    sql = f"select name, type from system.columns where table='{tb_name}';"
    df = read_sql(sql)
    df = df.set_index("name")
    type_dict = df.to_dict("dict")["type"]
    return type_dict


def to_sql(df, tb_name):
    type_dict = get_type_dict(tb_name)
    columns = list(type_dict.keys())
    # 類型處理
    for i in range(len(columns)):
        col_name = columns[i]
        col_type = type_dict[col_name]
        if "Date" in col_type:
            df[col_name] = pd.to_datetime(df[col_name])
        elif "Int" in col_type:
            df[col_name] = df[col_name].astype("int")
        elif "Float" in col_type:
            df[col_name] = df[col_name].astype("float")
        elif col_type == "String":
            df[col_name] = df[col_name].astype("str").fillna("")
    # df數據存入clickhouse
    cols = ",".join(columns)
    data = df.to_dict("records")
    client.execute(f"INSERT INTO {tb_name} ({cols}) VALUES", data, types_check=True)


if __name__ == '__main__':
    print(client.execute("SHOW DATABASES"))
    client.execute("DROP TABLE IF EXISTS test")
    print(client.execute("SHOW TABLES"))
    client.execute("CREATE TABLE test (x Int32) ENGINE = Memory")
    print(client.execute("SHOW TABLES"))

    client.execute("INSERT INTO test (x) VALUES", [{"x": 100}])
    client.execute("INSERT INTO test (x) VALUES", [[200]])
    client.execute(
        "INSERT INTO test (x) " "SELECT * FROM system.numbers LIMIT %(limit)s", {"limit": 3}
    )
    df = get_wespai_data("https://stock.wespai.com/p/48812")
    print(df.to_markdown())

GUI TOOL

https://dbeaver.io/download/

https://dbeaver.io/files/dbeaver-ce-latest-linux.gtk.x86_64.tar.gz

使用 Pandas 讀寫 ClickHouse

Pandas 本身不支援 ClickHouse 相關操作, 本篇部落格主要是記錄一下自己封裝的一些讀寫操作, 所以嚴格來說標題應該是: 如何從 ClickHouse 讀取資料到 DataFrame 及將 DataFrame 寫入 ClickHouse.

一. Pandas 讀取 ClickHouse

GitHub 上面有幾個 Python 操作 ClickHouse 的開放原始碼專案, 不過收藏數量都不多(幾百個 star), 經過試用, 發現 clickhouse_driver 這個項目相對靠譜一點, 先使用 pip 安裝 clickhouse_driver, 讀取資料的程式碼如下:

from clickhouse_driver import Client
import pandas as pd
import re

client = Client(host="xxx", database="xxx", user="xxx", password="xxx")

def read_sql(sql):
    data, columns = client.execute(sql, columnar=True, with_column_types=True)
    df = pd.DataFrame({re.sub(r"\W", "_", col[0]): d for d, col in zip(data, columns)})
    return df

使用 ClickHouse 的查詢命令也可以將資料讀出, 而且速度特別快, 但是得到的資料是一個超長的字串, 想要轉成 DataFrame 的話需要自己做切分和轉換, 操作比較麻煩, 以後有時間再嘗試, 查詢資料的程式碼如下:

import subprocess

def read_sql(sql):
    cmd = f'clickhouse-client -t --query "{sql}"'
    data = subprocess.getoutput(cmd)
    return data

二. Pandas 寫入 ClickHouse

由於 Pandas 的資料類型跟 ClickHouse 不同, 所以有些資料類型需要做一些處理, 大概的思路是

  • 獲取 ClickHouse 中指定表的各個欄位的資料類型

  • 修改將要寫入 ClickHouse 的 DataFrame 的資料類型

  • 將 DataFrame 裝換為字典形式

  • 呼叫 ClickHouse 的 insert 命令將字典資料匯入庫中*

程式碼實現如下:

from clickhouse_driver import Client
import pandas as pd
import re

client = Client(host="xxx", database="xxx", user="xxx", password="xxx")


def read_sql(sql):
    data, columns = client.execute(sql, columnar=True, with_column_types=True)
    df = pd.DataFrame({re.sub(r"\W", "_", col[0]): d for d, col in zip(data, columns)})
    return df


def get_type_dict(tb_name):
    sql = f"select name, type from system.columns where table='{tb_name}';"
    df = read_sql(sql)
    df = df.set_index("name")
    type_dict = df.to_dict("dict")["type"]
    return type_dict


def to_sql(df, tb_name):
    type_dict = get_type_dict(tb_name)
    columns = list(type_dict.keys())
    # 類型處理
    for i in range(len(columns)):
        col_name = columns[i]
        col_type = type_dict[col_name]
        if "Date" in col_type:
            df[col_name] = pd.to_datetime(df[col_name])
        elif "Int" in col_type:
            df[col_name] = df[col_name].astype("int")
        elif "Float" in col_type:
            df[col_name] = df[col_name].astype("float")
        elif col_type == "String":
            df[col_name] = df[col_name].astype("str").fillna("")
    # df數據存入clickhouse
    cols = ",".join(columns)
    data = df.to_dict("records")
    client.execute(f"INSERT INTO {tb_name} ({cols}) VALUES", data, types_check=True)


Pandas 寫入 clickhouse

from clickhouse_driver import Client
from io import StringIO
import numpy as np
import pandahouse as ph
import requests
import pandas as pd


def get_wespai_data(url):
    headers = {
        "user-agent": "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36"
    }
    TAIGU1 = requests.get(url, headers=headers)
    TAIGU1.encoding = "utf-8"
    raw_data = pd.read_html(StringIO(TAIGU1.text))[0]
    return raw_data


def insert_df_to_ch(df, str_database, str_table, bool_drop=True):
    dtypes_dict = dict(df.dtypes)
    dict_dtype = {}
    ch_type_convert_dict = {
        np.dtype("datetime64[ns]"): "Datetime64",
        np.dtype("int64"): "Int64",
        np.dtype("float64"): "Float64",
        np.dtype("object"): "String",
        np.dtype("bool"): "Bool",
    }

    create_table_cmd_str = ""
    for x in dtypes_dict:
        type_str = ch_type_convert_dict.get(dtypes_dict[x], None)
        if type_str is None:
            print(f"Undefined type {dtypes_dict[x]}")
        create_table_cmd_str = create_table_cmd_str + f"`{x}` {type_str} DEFAULT 0, "

    create_table_cmd_str = f"CREATE TABLE IF NOT EXISTS {str_database}.{str_table} ( {create_table_cmd_str[:-2]}) ENGINE = Log"
    # print(create_table_cmd_str)

    client = Client(host="localhost", port="9000", user="default", password="f0409")
    client.execute(f'CREATE DATABASE IF NOT EXISTS {str_database};')
    if bool_drop:
        client.execute(f'DROP TABLE IF EXISTS {str_database}.{str_table};')
    client.execute(create_table_cmd_str)

    connection = dict(
        database=str_database, host="http://127.0.0.1:8123/", user="default", password="f0409"
    )

    ph.to_clickhouse(df, str_table, index=False, chunksize=100000, connection=connection)


if __name__ == '__main__':
    df = get_wespai_data("https://stock.wespai.com/p/48812")
    #print(df)
    insert_df_to_ch(df, "CRYPTO", "TEST11" + "_MM_TEST", True)


組態檔案路徑:/etc/clickhouse-server/,默認的組態檔案為config.xml,但是建議將更新的組態放入config.d資料夾下,以防版本更新導致修改檔案被覆蓋。

默認資料保存路徑:/var/lib/clickhouse/

如果改路徑下的儲存空間比較小,建議將其該為大儲存空間下的目錄,例如:/app/clickhouse

注意:請注意/app/clickhouse的存取權,需要賦予clickhouse使用者的存取權,也可以直接使用chmod 775 /app/clickhouse為其設定寫存取權。


mkdir clickhouse_data/ 
sudo chown -R clickhouse:clickhouse clickhouse_data/
vim /etc/clickhouse-server/config.xml
<path>/home/shihyu/clickhouse_data/</path>
systemctl stop clickhouse-server
systemctl restart clickhouse-server
sudo systemctl status clickhouse-server

刪除XX日期之前的數據

from clickhouse_driver import Client
from datetime import datetime, timedelta

client = Client(
    host="localhost",
    port="9000",
    user="default",
    password="f0409",
    settings={"allow_experimental_lightweight_delete": True},
)
str_database = "CRYPTO"
str_table = "Bitopro_Orderbook_Partition"

# 計算三個月前的日期
three_months_ago = datetime.now() - timedelta(days=90)
print(three_months_ago.strftime("%Y-%m-%d %H:%M:%S"))

# 構建 SQL 語句
# sql = f"DELETE FROM {str_database}.{str_table} WHERE date < toYYYYMMDD('{three_months_ago.strftime('%Y-%m-%d %H:%M:%S')}');"
# sql = "DELETE FROM CRYPTO.Binance_Orderbook_Partition WHERE date < '2023-01-27 10:24:30' "
sql = f"DELETE FROM CRYPTO.Binance_Orderbook_Partition WHERE date < '{three_months_ago.strftime('%Y-%m-%d %H:%M:%S')}' "
print(sql)

# 執行 SQL 語句
client.execute(sql)

ClickHouse Partition

在 ClickHouse 中,Partition 是一種將表格分割成小塊的技術。可以在表格中設置一個或多個 Partition,這樣它們就可以存儲在不同的位置中。當查詢表格時,ClickHouse 能夠根據分區策略只查詢所需的分區,這樣就能夠提高查詢效率、降低維護成本。

在 ClickHouse 中,支持以下多種 Partition:

  1. Range Partition: 根據特定的欄位和一個或多個範圍,將表格分割成多個 Partition,並將每個 Partition 存儲在一個獨立的目錄中。
  2. List Partition: 根據特定的欄位和一個或多個值的列表,將表格分割成多個 Partition,並將每個 Partition 存儲在一個獨立的目錄中。
  3. Hash Partition: 將表格分割成多個 Partition,並根據 Hash 函數將每個 Partition 存儲在不同的磁盤上。
  4. Unpartitioned Table: 不對表格進行分區。

分區可以幫助 ClickHouse 實現更精確的查詢,因為可以通過分區信息更快地定位查找對象。例如,當表格被分成多個 Partition 時,ClickHouse 可以只查詢特定的 Partition,而非查詢整個表格,從而提高查詢效率。此外,當需要刪除 Partition 時,也不需要刪除整個表格。

總結來說,ClickHouse 中的 Partition 可以幫助你提高查詢效率、降低維護成本、以及增強表格的可用性和可擴展性。

How To Install dolphin on Ubuntu 20.04

sudo apt-get update
sudo apt-get -y install dolphin
sudo apt -y install dolphin

https://github.com/dolphindb/api_python3

把Sqlite當嵌入式KV資料庫用

https://zhuanlan.zhihu.com/p/93969678

市面上已經有很優秀的嵌入式KV資料庫了,如Berkeley DB。為什麼還需要把Sqlite當KV資料庫用呢?原因若干。

1,可能是為了好玩或者純屬無聊

2,可結合關係型資料庫與KV資料庫的優點

3,可利用一些sqlite特性做其他KV資料庫不好做的事情

4,事務管理更方便

5,sqlite更可靠,更流行

實現思路

使用json(或pickle)dump資料,並將資料寫入有KEY(主鍵)和VALUE兩個欄位的SQLITE庫表中。參照kv資料庫呼叫辦法實現外部介面。

主要功能

1,put:寫入key/value資料

2,get:獲取某個key的value

3,put_many:批次寫入key/value資料

4,keys:獲取所有key的列表

5,value:獲取所有value的列表

6,limit:利用SQL語句中limit關鍵字,獲取資料庫中“前”N條KV資料

7,random:利用SQL語句中random關鍵字,從資料庫中隨即獲取N條KV資料

8,has_key:某個key是否存在

9,cursor_execute:執行sql自訂語句

10,其他:items,pop,filter,count等

import os
import json
import sqlite3
import sys
from threading import Lock

PY3 = sys.version_info >= (3,)
if PY3:
    ifilter = filter
else:
    from itertools import ifilter

DUMPS = lambda d: json.dumps(d)
LOADS = lambda d: json.loads(d)


class SDB(object):
    _DEFAULT_TABLE = "__KVS_DEFAULT_TABLE__"
    _MEMORY_DB = ":memory:"

    def __init__(self, filename):
        if filename is None or len(filename) < 1 or filename.lower() == self._MEMORY_DB:
            self.filename = self._MEMORY_DB
        else:
            self.filename = filename
        self._lock = Lock()
        self._db_init()

    def _row_factory(self, cursor, row):
        result = []
        for idx, col in enumerate(cursor.description):
            if col[0].lower() in ("k", "v"):
                result.append(LOADS(row[idx]))
            else:
                result.append(row[idx])
        return result

    def _db_init(self):
        _new_table = "CREATE TABLE IF NOT EXISTS {0} ( k PRIMARY KEY,v)".format(
            self._DEFAULT_TABLE
        )
        db = sqlite3.connect(self.filename, timeout=60, check_same_thread=False)
        db.row_factory = self._row_factory
        db.execute(_new_table)
        self._cursor = db.cursor()
        self._db = db

    def _statement_init(self):
        table = self._DEFAULT_TABLE
        return dict(
            insert="insert or replace into {0}(k,v) values(:1,:2)".format(table),
            delete="delete from {0} where k=:1".format(table),
            update="update {0} set v=:1 where k=:2".format(table),
            clear="delete from {0}".format(table),
            get="select v from {0} where k=:1".format(table),
            has_key="select count(1) from {0} where k=:1".format(table),
            keys="select k from {0}".format(table),
            values="select v from {0}".format(table),
            items="select k,v from {0}".format(table),
            count="select count(*) from {0}".format(table),
            random="select * from {0} order BY RANDOM() limit :1".format(table),
            limit="select * from {0} limit :1 offset :2".format(table),
        )

    _statements = property(_statement_init)
    del _statement_init

    def _commit(self):
        self._db.commit()

    def _rollback(self):
        self._db.rollback()

    def _insert(self, key, value):
        try:
            self._lock.acquire(True)
            self._cursor.execute(
                self._statements.get("insert"), (DUMPS(key), DUMPS(value))
            )
        finally:
            self._lock.release()

    def _update(self, key, value):
        try:
            self._lock.acquire(True)
            self._cursor.execute(
                self._statements.get("update"), (DUMPS(value), DUMPS(key))
            )
        finally:
            self._lock.release()

    def _delete(self, key):
        try:
            self._lock.acquire(True)
            self._cursor.execute(self._statements.get("delete"), (DUMPS(key),))
        finally:
            self._lock.release()

    def _query(self, method, *args):
        try:
            self._lock.acquire(True)
            return self._cursor.execute(self._statements.get(method), args)
        finally:
            self._lock.release()

    def _clear(self):
        """
        刪除所有數據,需要調用_commit方法確認刪除
        :return:
        """
        try:
            self._lock.acquire(True)
            self._cursor.execute(self._statements.get("clear"))
        except Exception as e:
            self._rollback()
            raise e
        finally:
            self._lock.release()

    def keys(self, sort=False, sort_key=None, reverse=False):
        if sort:
            return sorted(self.iterkeys(), key=sort_key, reverse=reverse)
        return list(self.iterkeys())

    def values(self, sort=False, sort_key=None, reverse=False):
        if sort:
            return sorted(self.itervalues(), key=sort_key, reverse=reverse)
        return list(self.itervalues())

    def iterkeys(self):
        for k in self._query("keys"):
            yield k[0]

    def itervalues(self):
        for k in self._query("values"):
            yield k[0]

    def items(self, sort=False, key=None, reverse=False):
        if sort:
            return sorted(self.iteritems(), key=key, reverse=reverse)
        return list(self.iteritems())

    def iteritems(self):
        for k, v in self._query("items"):
            yield k, v

    def count(self):
        return self._query("count").fetchone()[0]

    def has_key(self, key):
        return self._query("has_key", DUMPS(key)).fetchone()[0] > 0

    def get(self, key):
        data = self._query("get", DUMPS(key)).fetchone()
        if data:
            return data[0]

    def put(self, key, value):
        try:
            self._insert(key, value)
            self._commit()
        except Exception as e:
            self._rollback()
            raise e

    def pop(self, key):
        try:
            value = self.get(key)
            self._delete(key)
            self._commit()
            return value
        except Exception as e:
            self._rollback()
            raise e

    def put_many(self, rows):
        try:
            self._lock.acquire(True)
            if rows and len(rows) > 0:
                self._cursor.executemany(
                    self._statements.get("insert"),
                    [(DUMPS(k), DUMPS(v)) for k, v in rows],
                )
                self._commit()
        except Exception as e:
            self._rollback()
            raise e
        finally:
            if self._lock.locked():
                self._lock.release()

    def limit(self, limit=1, offset=0):
        rows = self._query("limit", limit, offset)
        if limit == 1:
            return rows.fetchone()
        return rows.fetchall()

    def random(self, limit=1):
        rows = self._query("random", limit)
        if limit == 1:
            return rows.fetchone()
        return rows.fetchall()

    def filter(self, func):
        return list(ifilter(func, self.items()))

    def ifilter(self, func):
        return ifilter(func, self.iteritems())

    def cursor_execute(self, sql, parameters=None):
        """
        執行SQL語句,如:SELECT K,V FROM __KVS_DEFAULT_TABLE__ WHERE K LIKE 'ABC%'
        """
        try:
            self._lock.acquire(True)
            return self._cursor.execute(sql=sql, parameters=parameters)
        finally:
            self._lock.release()

    def close(self):
        try:
            self._rollback()
            self._cursor.close()
            self._db.close()
        except:
            pass


if __name__ == "__main__":
    # 打開資料庫
    db = SDB("test.sqlite")
    # 寫入單條資料
    db.put("first", "第一條資料")
    db.put("second", dict(a=1, b=2, c=[2, 3, 4]))
    # 獲取資料
    db.get("first")
    # 寫入多條資料
    db.put_many([[1, 2], [3, 4], ["A", "abc"]])
    # 獲取key的列表
    db.keys()

CryptoTrade

Python Binance API 教學

幣安帳務下單& websocket

import asyncio
import json
import websockets
from binance.client import Client
from binance.enums import *

class mid_class:
    def __init__(self, api_key, api_secret):
        self.client = Client(api_key, api_secret)
        self.balance = None
        self.ticker = None
        self.depth = None
        self.order = None

    def get_account(self):
        self.balance = self.client.get_asset_balance(asset='USDT')

    def get_ticker(self):
        self.ticker = self.client.get_symbol_ticker(symbol='BTCUSDT')

    def get_depth(self):
        self.depth = self.client.get_order_book(symbol='BTCUSDT')

    def create_order(self):
        self.order = self.client.create_test_order(
            symbol='BTCUSDT',
            side=SIDE_BUY,
            type=ORDER_TYPE_LIMIT,
            timeInForce=TIME_IN_FORCE_GTC,
            quantity=100,
            price='1000')

    def cancel_order(self):
        self.client.cancel_order(
            symbol='BTCUSDT',
            orderId=self.order['orderId'])

async def main():
    api_key = 'your_api_key'
    api_secret = 'your_api_secret'
    mid = mid_class(api_key, api_secret)
    #mid.get_account()
    #mid.get_ticker()
    #mid.get_depth()
    #mid.create_order()
    #mid.cancel_order()
    await fetch_depth(mid)

async def fetch_depth(mid):
    async with websockets.connect('wss://stream.binance.com:9443/ws/btcusdt@depth@100ms') as websocket:
        while True:
            depth_json = await websocket.recv()
            depth_dict = json.loads(depth_json)
            print(depth_dict)

if __name__ == '__main__':
    asyncio.run(main())

使用Websocket 訂閱幣安 orderbook 100ms

import asyncio
import json
import websockets

async def fetch_depth(symbol):
    while True:
        try:
            async with websockets.connect(f'wss://stream.binance.com:9443/ws/{symbol}@depth@100ms') as websocket:
                while True:
                    depth = await websocket.recv()
                    depth = json.loads(depth)
                    print(depth)
        except websockets.exceptions.ConnectionClosed:
            print(f'Connection to {symbol} WebSocket closed, reconnecting...')

async def main():
    tasks = [
        asyncio.create_task(fetch_depth('btcusdt')),
        asyncio.create_task(fetch_depth('ethusdt'))
    ]
    await asyncio.gather(*tasks)

asyncio.run(main())

如何獲取 SMA 等技術指標?

我們已經討論瞭如何將 DataFrame 輸出為 CSV 檔。你可以用 Python Pandas 做更多的事情,計算移動平均線就是其中之一。

下面是一個示例:

import btalib
import pandas as pd

# load DataFrame
btc_df = pd.read_csv('btc_bars3.csv', index_col=0)
btc_df.set_index('date', inplace=True)
btc_df.index = pd.to_datetime(btc_df.index, unit='ms')

# calculate 20 moving average using Pandas
btc_df['20sma'] = btc_df.close.rolling(20).mean()
print(btc_df.tail(5))

在上面的代碼中,我們從之前創建的CSV檔載入了數據。然後,我們使用mean() 函數來計算收盤列上的平均值。

滾動函數允許我們為移動平均線設置一個週期。所有這些內容都將附加到現有的數據幀中。這就是結果的樣子。

img

如您所見,已使用 20 移動平均線創建了一個新列。

假設您只需要知道移動平均線現在的位置。或者從數據幀中的最後一個價位開始。

我們可以使用相同的 mean() 函數,只需在 DataFrame 的最後 20 行上運行它,如下所示:

# calculate just the last value for the 20 moving average
mean = btc_df.close.tail(20).mean()

熊貓可以做的還有很多。我們可以很容易地抓住今年交易的最高價格比特幣,如下所示 –

# get the highest closing price in 2020
max_val = btc_df.close['2020'].max()

但Pandas無法計算其他技術指標,如RSI或MACD。幣安 API 也不提供此資訊。

TA-LIB一直是一個流行的庫一段時間了。我們最近有機會測試了一個新的庫 – bta-lib。

該庫由Backtrader的作者創建。他在博客上討論TA-LIB有幾個指標實施不當。

此外,TA-LIB不是為Python設計的。有一個可用的包裝器,但是為Python設計的解決方案的開銷要少得多。

可以使用 PIP 安裝 Bta-lib,如下所示。

pip install bta-lib

讓我們嘗試用庫計算相同的移動平均線作為比較 –

sma = btalib.sma(btc_df.close)
print(sma.df)

現在,我們有一個單獨的數據幀,其中包含移動平均線的值。它看起來像這樣:

img

請注意,bta-lib 將返回一個對象到我們的 sma 變數。若要訪問其中包含的數據幀,只需在變數名稱後鍵入即可。.df

默認情況下,該庫使用 30 週期移動平均線。

我們可以複製我們之前的相同函數,並計算20條移動平均線,並將其作為列附加到我們原來的DataFrame上,就像這樣。

# create sma and attach as column to original df
btc_df['sma'] = btalib.sma(btc_df.close, period=20).df
print(btc_df.tail())

讓我們再創建幾個指標。以下是我們使用bta-lib庫計算RSI的方法 –

rsi = btalib.rsi(btc_df, period=14)

再次返回包含 df 的物件。我們可以像這樣訪問最後一個值。

print(rsi.df.rsi[-1])

在實時環境中,您可能只需要最後一個值。

以下是我們如何計算bta-lib中比特幣的MACD。

macd = btalib.macd(btc_df, pfast=20, pslow=50, psignal=13)

最後,我們將把RSI和MACD值加入到我們的原始比特幣價格DataFrame中。

# join the rsi and macd calculations as columns in original df
btc_df = btc_df.join([rsi.df, macd.df])
print(btc_df.tail())

現在,我們可以從一個數據幀輕鬆訪問所有計算 –

img

如何使用幣安API觸發乙太坊訂單?

我們使用的庫具有一個函數,允許我們創建測試訂單。下面是一個示例:

buy_order_limit = client.create_test_order(
    symbol='ETHUSDT',
    side='BUY',
    type='LIMIT',
    timeInForce='GTC',
    quantity=100,
    price=200)

我們可以確保我們的語法是正確的,而不必提交實時訂單。當您瞭解 API 時,這非常有用。

例如,如果我們將上述代碼中的類型更改為”MARKET”,它將引發異常。原因是timeInForce價格參數不用於市場訂單。相反,市場訂單將如下所示:

buy_order = client.create_test_order(symbol='ETHUSDT', side='BUY', type='MARKET', quantity=100)

一旦您滿意語法正確,只需將 替換為 .create_test_order function``create_order function

注意:如果您按照示例進行操作,則在將上述限價訂單代碼用於ETHUSDT時,如果自編寫本文以來價格已大幅變動,則可能會收到API錯誤。幣安只允許與硬幣當前交易價格相差一定百分比的訂單。

由於可能存在異常,因此我們將代碼包裝在 try/except 塊中,並從庫中導入一些定義的異常。

import os

from binance.client import Client
from binance.enums import *
from binance.exceptions import BinanceAPIException, BinanceOrderException

# init
api_key = os.environ.get('binance_api')
api_secret = os.environ.get('binance_secret')

client = Client(api_key, api_secret)

除了用戶端和自定義異常之外,我們還導入了 binance.enums,我們稍後將對此進行討論。

這是訂單代碼塊。

# create a real order if the test orders did not raise an exception

try:
    buy_limit = client.create_order(
        symbol='ETHUSDT',
        side='BUY',
        type='LIMIT',
        timeInForce='GTC',
        quantity=100,
        price=200)

except BinanceAPIException as e:
    # error handling goes here
    print(e)
except BinanceOrderException as e:
    # error handling goes here
    print(e)

訂單確認將從交易所發回並存儲在我們的buy_limit變數中。這是它的樣子:

img

它是字典格式。請注意,它包含一個 orderId。我們可以使用此ID來取消像這樣的限價訂單 –

    # cancel previous orders
    cancel = client.cancel_order(symbol='ETHUSDT', orderId=buy_limit['orderId'])

我們再次收到確認。我們可以列印出 cancel 變數來查看它。

img

該函數是下訂單的主要方法。我們可以在這裡傳遞幾個參數。create_order

但是有一些常見的順序,並且已經為它們創建了説明器函數。它們縮短了下訂單所需的代碼,使事情變得容易一些。以下是一些範例:

    # same order but with helper function
    buy_limit = client.order_limit_buy(symbol='ETHUSDT', quantity=100, price=200)

    # market order using a helper function
    market_order = client.order_market_sell(symbol='ETHUSDT', quantity=100)

以下是您可能希望使用的一些幫助程式函數:

  • order_limit_buy()
  • order_limit_sell()
  • order_market_buy()
  • order_market_sell()
  • order_oco_buy()
  • order_ocosell()

最後兩個被視為高級訂單類型。OCO代表One Cancels the Other。

一個很好的例子是當您使用止損和止盈目標時。如果其中一個訂單被擊中,您可能希望另一個訂單被取消。

某些訂單類型需要字串常量,例如**「MARKET」「BUY」。。**另一個經紀人可能會使用「MKT」 所以關於你應該使用什麼並不總是有一個合乎邏輯的答案。

如果需要,您可以在文檔中查找這些內容。或者,該庫將硬編碼字串轉換為您可以使用的變數。

如果您的編碼編輯器具有自動完成功能,這將特別有用,因為您可以快速確定要使用的參數,而無需拉出文檔。

下面是一個不使用內置變數的訂單示例:

buy_order = client.create_test_order(symbol='ETHUSDT', side='BUY', type='MARKET', quantity=100)

使用內置變數也是一樣的。

buy_order = client.create_test_order(symbol='ETHUSDT', side=SIDE_BUY, type=ORDER_TYPE_MARKET, quantity=100)

如果計劃採用此路由,則需要前面討論的枚舉導入。

所有硬編碼字串的完整清單可以在這裡找到。

如何使用幣安API實現止損或止盈?

與其他市場(如股票或外匯)相比,加密貨幣的止損或止盈方法不同。

原因是,對於股票,你有一個基礎貨幣。這通常以美元為單位。一旦你買了一隻股票,你就處於”交易”中。在某些時候,您將希望出售該股票並返回到您的基礎美元貨幣。

對於加密貨幣,實際上沒有基礎貨幣的概念。當您進行交易時,您正在將一種貨幣換成另一種貨幣。系統不會將其視為您最終想要擺脫的”交易”。

因此,幣安不允許您將止損和止盈本機附加到主訂單上。

但是我們仍然可以手動實現一個。

為此,我們可以下OCO訂單。這個想法是,如果止損或止盈被擊中,另一個訂單應該被取消。

回到我們的ETH訂單,以下是我們如何實現止損和止盈。

try:
    order = client.create_oco_order(
        symbol='ETHUSDT',
        side='SELL',
        quantity=100,
        price=250,
        stopPrice=150,
        stopLimitPrice=150,
        stopLimitTimeInForce='GTC')

except BinanceAPIException as e:
    # error handling goes here
    print(e)
except BinanceOrderException as e:
    # error handling goes here
    print(e)

請注意,我們正在傳遞止損價格止損限價。一旦達到止損價格水準,將使用止損限價。在大多數情況下,這兩個參數的價格將相同。

雖然大多數資產都接受止損限價單,但並非所有資產都接受止損限價單。在下訂單之前,最好檢查它是否受支援。

為此,可以使用交換信息終結點。

# use exchange info to confirm order types
info = client.get_symbol_info('ETHUSDT')
print(info['orderTypes'])

以下是回應 –

img

orderTypes下,它表明該資產確實接受止損限值。

這裡還有其他有用的資訊,例如資產是否可以以保證金交易,最小數量和價格變動大小。

如何使用幣安幣(BNB)獲得交易手續費折扣?

幸運的是,幣安有一個交易手續費折扣計劃。

img

The image above shows the fee schedule and discounts for trading the spot market. There are discounts for futures trading too.

You can either qualify for a discount depending on your trading volume or the quantity of Binance coin you own.

Binance coin or BNB was created by Binance in 2017. It can be used as a currency although perhaps the more common usage for it is to pay trading fees.

If you’re not keen on owning BNB, it still makes sense to own just a little bit to pay your trading fees with. After all, any amount of BNB will qualify you for the first tier.

Keep in mind, if you’re using BNB to pay for trading fees, your balance will reduce over time.

The function below ensures there is a minimum amount of BNB in your account and tops it up if there isn’t.

def topup_bnb(min_balance: float, topup: float):
	''' Top up BNB balance if it drops below minimum specified balance '''
	bnb_balance = client.get_asset_balance(asset='BNB')
	bnb_balance = float(bnb_balance['free'])
	if bnb_balance < min_balance:
		qty = round(topup - bnb_balance, 5)
		print(qty)
		order = client.order_market_buy(symbol='BNBUSDT', quantity=qty)
		return order
	return False

Trading scripts are usually run in a loop, so periodically calling the above function will ensure there is enough BNB in the account to qualify for the minimum discount.

As an example, we can call the above function like this –

min_balance = 1.0
topup = 2.5
order = topup_bnb(min_balance, topup)

這將檢查至少1 BNB的餘額。如果BNB的金額低於此值,它將使其達到2.5 BNB。

要使用BNB支付交易費用並獲得折扣,需要啟用它。在幣安主頁面中,登錄後按兩下右上角的個人資料圖標。

第一個選項應該是您的電子郵件位址,按下該位址以訪問您的儀錶板。從那裡,將有一個看起來像這樣的部分 –

img

在這裡,您可以啟用和禁用使用BNB支付交易費用的選項。

當BTC達到一定價格時,如何在ETH上執行交易?

在下一個示例中,當比特幣突破10,000美元的價格點時,我們將在乙太坊中下達買入訂單。

我們將使用Binance WebSocket來跟蹤比特幣的價格。

import os
from time import sleep

from binance.client import Client
from binance import ThreadedWebsocketManager

# init
api_key = os.environ.get('binance_api')
api_secret = os.environ.get('binance_secret')
client = Client(api_key, api_secret)
price = {'BTCUSDT': None, 'error': False}

上面的代碼看起來與前面的示例非常相似,我們在前面的例子中展示瞭如何使用 WebSocket。

def btc_pairs_trade(msg):
    ''' define how to process incoming WebSocket messages '''
    if msg['e'] != 'error':
        price['BTCUSDT'] = float(msg['c'])
    else:
        price['error'] = True

接下來,我們有回調函數。這是所有 WebSocket 數據流經的地方。我們也可以在這裡對交易邏輯進行程式設計。

但是,由於我們需要對訂單輸入使用 try/except 塊,因此最好不要這樣做,因為這可能會干擾在庫中的後端進行的錯誤檢查。

我們將啟動 WebSocket 並將其定向到我們剛剛創建的函數。btc_pairs_trade

bsm = ThreadedWebsocketManager()
bsm.start()
bsm.start_symbol_ticker_socket(symbol='BTCUSDT', callback=btc_pairs_trade)

在開始之前,請快速檢查以確保我們有數據。

while not price['BTCUSDT']:
    # wait for WebSocket to start streaming data
    sleep(0.1)

一旦WebSocket用新值填充我們的價格字典,上述無限迴圈就會中斷。

關於主要的交易邏輯。

while True:
    # error check to make sure WebSocket is working
    if price['error']:
        # stop and restart socket
	bsm.stop()
	sleep(2)
	bsm.start()
        price['error'] = False

    else:
        if price['BTCUSDT'] > 10000:
            try:
                order = client.order_market_buy(symbol='ETHUSDT', quantity=100)
                break
            except Exception as e:
                print(e)

    sleep(0.1)

在這裡,我們正在檢查價格是否高於我們的參數,在這種情況下為10,000美元。如果是這樣,我們會發送市場訂單以購買ETHUSDT。

在發送買入訂單後,我們打破迴圈,我們的腳本完成。

不要忘記正確終止 WebSocket

bsm.stop()

當BTC在過去5分鐘內移動5%時,如何執行ETH交易?

我們將再次做出基於比特幣的乙太坊交易決策。儘管在此示例中,我們正在尋找過去五分鐘內大於5%的價格變動。

因此,如果比特幣漲幅超過5%,我們就會買入乙太坊。如果它下跌超過5%,我們將出售乙太坊。

由於我們可能在這裡持有空頭頭寸,因此我們將交易期貨。在現貨市場上,只有當您已經擁有該加密貨幣時,您才能出售。

我們的導入和腳本的大部分初始部分都沒有改變。這裡的主要區別在於我們使用Pandas,因為我們將從WebSocket的傳入數據存儲到DataFrame中。

import os
from time import sleep

import pandas as pd
from binance import ThreadedWebsocketManager
from binance.client import Client

# init
api_key = os.environ.get('binance_api')
api_secret = os.environ.get('binance_secret')
client = Client(api_key, api_secret)
price = {'BTCUSDT': pd.DataFrame(columns=['date', 'price']), 'error': False}

因此,我們導入了Pandas,並在價格字典中創建了一個空白的數據幀。數據幀有兩列,一列用於日期,或者更確切地說是時間。另一列將持有價格。

回調函數包含用於從 WebSocket 數據填充數據幀的代碼。

def btc_pairs_trade(msg):
	''' define how to process incoming WebSocket messages '''
    if msg['e'] != 'error':
        price['BTCUSDT'].loc[len(price['BTCUSDT'])] = [pd.Timestamp.now(), float(msg['c'])]
    else:
        price['error'] = True

我們使用該函數通過最後一個索引值將數據追加到 DataFrame 中。我們使用數據幀的長度來確定索引值。.loc

此時,我們只是插入當前時間(使用 Pandas 中的時間戳函數獲得)和來自套接字流的價格。

現在我們已經創建了回調函數,我們將啟動 WebSocket。

# init and start the WebSocket
bsm = ThreadedWebsocketManager()
bsm.start()
bsm.start_symbol_ticker_socket(symbol='BTCUSDT', callback=btc_pairs_trade)

再一次,我們將進行快速檢查,以確保數據正在流式傳輸。

## main
while len(price['BTCUSDT']) == 0:
	# wait for WebSocket to start streaming data
	sleep(0.1)
	
sleep(300)

在開始主交易邏輯之前,我們將把腳本休眠五分鐘,因為我們至少需要那麼多數據。

while True:
	# error check to make sure WebSocket is working
    if price['error']:
        # stop and restart socket
        bsm.stop()
	sleep(2)
        bsm.start()
        price['error'] = False
    else:
		df = price['BTCUSDT']
		start_time = df.date.iloc[-1] - pd.Timedelta(minutes=5)
		df = df.loc[df.date >= start_time]
		max_price = df.price.max()
		min_price = df.price.min()

在主迴圈中,我們首先從字典檔中獲取DataFrame並將其分配給變數df。此步驟不是必需的,但會使我們的示例中的代碼更易於閱讀。

接下來,我們確定五分鐘前的時間。我們可以通過從DataFrame中獲取最後一個日期值並使用Pandas內置的Timedelta函數減去5分鐘來做到這一點。我們將此值分配給變數start_time。

使用start_time值,我們可以向下篩選數據幀,使其僅包含最後五分鐘的數據。

從那裡,我們可以使用Pandas的max()和min()函數來找到最高和最低的價格。

現在我們需要做的就是確定最後一個價格與最大值或最小值之間的變動是否大於5%。

		if df.price.iloc[-1] < max_price * 0.95:
			try:
				order = client.futures_create_order(symbol='ETHUSDT', side='SELL', type='MARKET', quantity=100)
				break
			except Exception as e:
				print(e)

		elif df.price.iloc[-1] > min_price * 1.05:
			try:
				order = client.futures_create_order(symbol='ETHUSDT', side='BUY', type='MARKET', quantity=100)
				break
			except Exception as e:
				print(e)

	sleep(0.1)

如果最新價格比上一個值大5%,我們知道比特幣正在上漲,我們將做空乙太坊作為我們均值回歸策略的一部分。

如果最後一個價格比數據幀中的最高價格低 5%,那麼我們就會反其道而行之。

請注意,該庫沒有期貨市場訂單的説明器函數,因此我們使用的方法類似於用於現貨市場的create_order函數。

再一次,如果我們的訂單被填滿,我們將打破我們的主循環並正確終止WebSocket。

# properly stop and terminate WebSocket
bsm.stop()

但是,你的策略可能是無限期運行的策略。如果您不打算在下訂單後突破,最好將腳本置於睡眠狀態一段時間。

否則,每逢價格變動時都會發送新訂單,直到 5% 的背離缺口關閉。

出處

https://www.coreenginepro.com/python/python-binance-api-%e6%95%99%e5%ad%b8-4-%e6%8a%80%e8%a1%93%e6%8c%87%e6%a8%99%e5%88%86%e6%9e%90/


from binance.client import Client
import pandas as pd
import matplotlib.pyplot as plt
import os
import json


class binanceAPI:
    def __init__(self, configPath):
        with open(configPath, "r") as f:
            self.kw_login = json.loads(f.read())
        self.api = self.__login(self.kw_login["PUBLIC"], self.kw_login["SECRET"])

    def __login(self, PUBLIC, SECRET):
        return Client(api_key=PUBLIC, api_secret=SECRET)


if __name__ == "__main__":
    cols = [
        "timestamp",
        "open",
        "high",
        "low",
        "close",
        "volume",
        "close_time",
        "quote_av",
        "trades",
        "tb_base_av",
        "tb_quote_av",
        "ignore",
    ]
    """
    Jan
    Feb
    Mar
    Apr
    May
    Jun
    Jul
    Aug
    Sep
    Oct
    Nov
    Dec
    """
    # client = binanceAPI(os.environ["HOME"] + f"/.mybin/jason/binance_login.txt")
    # klines = client.api.get_historical_klines(
    #    "BTCUSDT", "1m", "1 JAN, 2022", "20 JUN, 2023"
    # )
    # df = pd.DataFrame(klines, columns=cols)
    # df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
    # df["close"] = df["close"].astype(float)
    # df["change"] = round(df["close"].pct_change() * 100, 4)
    # df = df.set_index("timestamp")
    # print(df)
    # df.to_csv("BTCUSDT_1m.csv")

    # Load the DataFrame from the CSV file
    df = pd.read_csv("BTCUSDT_1m.csv", index_col="timestamp")
    df["change"] = df["change"].astype(float)

    # Create a line chart for the "change" column
    plt.plot(df["change"])

    # Add a title and axis labels
    plt.title("BTCUSDT 1m Change")
    plt.xlabel("Time")
    plt.ylabel("Change (%)")

    # Display the chart
    plt.show()

如何將OCO訂單發送到幣安

我想請你幫忙。我正在嘗試將python代碼從通過api到Binance發送限價/市價訂單更改為OCO訂單。我可以做限價單,市價單,止損限價單。我不知道該如何下OCO訂單...

當我使用限價單時,我發送的是order_type = ORDER_TYPE_LIMIT,然後我使用order = client.create_order(),它可以正常工作。當我想發送市價單時,我使用了order_type = ORDER_TYPE_MARKET,但是當我要進行OCO訂單時,我發現唯一可行的選擇是:order = client.create_oco_order()而沒有order_type,但是在這裡我遇到了錯誤1013止損不支持此符號...

我檢查了https://api.binance.com/api/v1/exchangeInfo

並有以下“ orderTypes”:[“ LIMIT”,“ LIMIT_MAKER”,“ MARKET”,“ STOP_LOSS_LIMIT”,“ TAKE_PROFIT_LIMIT”],“ icebergAllowed”:true,“ ocoAllowed”:true,

所以我不能使用order_type。沒有ORDER_TYPE_OCO,ocoAllowed為true,所以我應該能夠發送oco訂單。但是我收到“錯誤1013:此代碼不支持止損定單。定單失敗”。

我想要的是將“價格”設置為限價賣出訂單,以在價格到達那裡時獲得更高的獲利,並在價格下跌時將止損“ stopPrice”設置得更低... 這就是OCO的工作方式。

有人可以給我一個建議怎麼做嗎?我不是python專家,我只是在更改一個發現的代碼,我的理解是,如果允許oco,也應該允許止損。謝謝


為了使所有感興趣的人都能找到有關此問題的解決方案的準確答案,我將代碼包含在注釋中。

我將使用OCO賣單作為BTCUSDT中的示例。

假設我有1個BTC。當前價格為30157.85,我想在32000.07賣出更高的1個BTC

但是價格沒有上漲並開始下跌,因此我將止損價設置為29283.03,在該價格處以29000.00的價格開立限價賣單。

這意味著我將以32000.07或29000.00 USDT的價格賣出。該命令的編寫方式如下:

order= client.order_oco_sell(
    symbol= 'BTCUSDT',                                            
    quantity= 1.00000,                                            
    price= '32000.07',                                            
    stopPrice= '29283.03',                                            
    stopLimitPrice= '29000.00',                                            
    stopLimitTimeInForce= 'FOK')

生效時間訂單

生效時間指的是您的訂單在被執行或過期之前維持有效的時間。這樣可以讓您更具體的掌握時間參數,您可以在下單時自訂時間。

幣安提供 GTC (有效直到取消)、IOC (立即成交或取消) 或 FOK (全部成交或取消) 等訂單選項:

  • GTC (有效直到取消):訂單將維持有效到成交或被您取消。
  • IOC (立即成交或取消):以可用價格及數量立即嘗試成交全部或部分訂單,然後取消剩餘未成交的訂單部分。如果您下單時所選擇的價格沒有可供應的數量,訂單將會立即被取消。請注意,此訂單類型不支持冰山委託。
  • FOK (全部成交或取消):訂單必須立即完全成交 (全部成交),否則將被取消 (完全取消)。請注意,此訂單類型不支持冰山委託。

請注意,OCO訂單需要stopLimitTimeInForce參數。我使用了'FOK'值,但在這裡給您留下了可以使用的不同值的描述:https : //help.bybit.com/hc/zh-CN/articles/360039749233-What-are-time-有效TIF-GTC-IOC-FOK-

請注意,price,stopPrice,stopLimitPrice和stopLimitTimeInForce參數是字符串,而不是浮點數或十進制數。

使用 Git LFS 上傳大型檔案

https://ithelp.ithome.com.tw/articles/10229654

專案開發過程中,檔案越來越大在所難免,GitHub 限制單一檔案 100 MB 的限制,這時候就需要交由 LFS 這個功能,來解決類似以下的錯誤訊息。

remote: warning: Large files detected.
remote: error: File large_file is 123.00 MB; this exceeds GitHub's file size limit of 100 MB

安裝 Git LFS

安裝命令

  • Linux

    依序輸入以下指令

curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash
sudo apt-get install git-lfs
git lfs install
  • MacOS

    依序輸入以下指令,如果不能執行 brew 相關指令,參考 這裡 安裝 HomeBrew 。

brew install git-lfs
git lfs install

執行安裝檔,安裝完畢後,回到專案終端機,輸入

git lfs install

將檔案交由 LFS 管理

* 表示所有檔案, .psd 表示副檔名為 .psd 的檔案,所以 lfs 會管理所有副檔名為 .psd 的檔案,若有多個附檔名要管理,請一一執行命令,或參考 這裡 的第四點。

git lfs track "*.psd"

接著 Push 專案

LFS 管理大型檔案後,繼續執行 git add git commit git push 命令即可。

出處: https://magiclen.org/git-remove-commited-files/

在使用Git進行程式專案或是其它任何專案的版本控制時,通常會使用.gitignore檔案來讓Git在使用add將檔案納入Git的版本控制清單的時,過濾掉指定的目錄或檔案。通常這些被過濾的檔案是由專案在進行建置時所產生出來的任何檔案,以及一些使用者需要自行修改或是建立的設定檔(例如存放資料庫登入資訊的設定檔)。但我們都是人,難免會忘記把要過濾掉的檔案加進.gitignore中,而使得該檔案不小心被commit進我們的倉庫之中。有時候不小心commit進去的甚至是大小超過數十MB的檔案,或是一些存放著帳號密碼、金鑰等與安全性直接相關的設定檔,這可就不太妙了。

一旦Git中的大檔愈多,在進行git clone的時候就會變得愈來愈慢,而且GitHub、BitBucket等主流的Git遠端倉庫服務商,通常會限制倉庫中單一檔案的大小與整個倉庫的大小。

您可能會問:難道不小心commit進Git的檔案,不是直接在專案底下刪掉後,再重新commit就好了嗎?……OK,我們來做個小小的實驗吧!

假設有個專案,只有Hello World這個二進制檔,存在於專案的根目錄中。我們先將目前工作目錄移動到這個專案的根目錄,接著使用以下指令來初始化Git。

git init

然後使用以下指令,將Hello World檔案加進Git的版本控制清單中。

git add Hello\ World

接著使用以下指令來進行commit。

git commit -m 'Hello!'

然後用以下指令將Hello World檔案從專案中刪除。

rm Hello\ World

然後再次使用以下指令,將專案的根目錄加進Git的版本控制清單中。

git add .

接著使用以下指令來進行commit。

git commit -m 'Delete Hello World'

然後執行以下指令,暫時回到前一個commit時的狀態。

git checkout HEAD~1

最後執行ls指令,列出目前專案根目錄底下有哪些檔案。

如上圖,看到沒?原本被我們的刪除的Hello World檔案又跑出來了!它根本就沒有被刪除嘛!

刪除Git中已經被commit的檔案

顯示目前專案的Git倉庫究竟佔用了多少空間

以下指令,可以顯示目前專案的Git倉庫所佔用的檔案空間:

git count-objects -vH

size欄位加上size-pack欄位所顯示的值就是Git倉庫所佔用的檔案空間。

如上圖,可知目前筆者這個Android專案的Git倉庫居然佔用了1.47 GiB的空間。

將Git倉庫中的檔案依照大小排序並顯示出來

以下指令,可以列出目前Git倉庫中的所有檔案,並且按照檔案大小來排序:

git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | sed -n 's/^blob //p' | sort --numeric-sort --key=2 | cut -c 1-12,41- | $(command -v gnumfmt || echo numfmt) --field=2 --to=iec-i --suffix=B --padding=7 --round=nearest

git-remove-commited-files

如上圖,基本上可以看出筆者的這個Git倉庫把Android所建置出來的APK檔也加進來了,所以才會那麼肥大。

這個指令有個要注意的地方是,如果有同樣的檔案但儲存在不同的路徑,只會顯示出其中一個路徑哦!在之後要刪除的時候必須要把每個路徑的該檔案都刪除,該檔案才會真正地從Git倉庫中移除。

確認要排除在Git之外的檔案,是否已經在.gitignore

以下指令,可以檢查要排除在Git之外的檔案,是否已經在.gitignore中:

git add . && git ls-files --error-unmatch 檔案路徑

這邊的檔案路徑是指檔案在Git倉庫中的路徑。

如果在執行指令後,看到螢幕上顯示:

error: pathspec '檔案路徑' did not match any file(s) known to git.

表示目前那個檔案已經在.gitignore中了。

將要排除在Git之外的檔案加進.gitignore

Git倉庫底下的.gitignore檔案可以設定要排除加入的檔案或是目錄。如果您不知道怎麼設定,可以在以下這個網站搜尋到很多針對不同程式語言和IDE用的.gitignore模板。

https://www.gitignore.io/

在修改完.gitignore後,執行以下指令來重新套用:

git rm -r --cached .

如果有需要的話,可以立刻再重新將專案根目錄加進版本控制清單中,並再commit一次,指令如下:

git add . && git commit -m 'update .gitignore'

將已經被commit進Git倉庫的檔案刪除

以下指令,可以把已經被commit進Git倉庫的檔案或整個目錄給刪除:

git filter-branch --force --tree-filter 'rm -f -r "檔案路徑"' -- --all

這邊的檔案路徑是指檔案在Git倉庫中的路徑,不支援wildcard寫法。注意這個指令會直接將有包含指定檔案的commit進行修改,而導致該commit的雜湊值以及這個commit之後的所有commit的雜湊值都會發生變化。

另外還一點要注意的是,在執行一次以上指令之後,就算有確實將指定的檔案刪除掉,再次使用這篇文章先前提到的將Git倉庫中的檔案顯示出來的指令,也可能還是會看到那個應該已經要被刪除掉的檔案仍然出現在清單中,它會在這個Git倉庫有其它變動(例如建立新的commit)後,檔案列表才會跟著刷新。

如果要立刻刷新檔案列表,可以執行以下指令:

git filter-branch --force

清除Git的Reflog並手動調用垃圾回收機制

以下指令,可以清除Git的Reflog並手動調用垃圾回收機制,來即時讓Git倉庫將可用空間釋放出來。如果並非迫切地需要釋出空間,建議不要執行這個指令。

git reflog expire --expire=now --expire-unreachable=now --all && git gc --prune=all --aggressive

覆蓋遠端的Git倉庫

若您已經不幸地將要排除在Git倉庫之外的檔案給上傳到遠端的Git倉庫,那麼就在處理好本地端的Git倉庫之後,使用以下指令來覆蓋遠端的commit吧!

git push --force --all

常用指令

  • git init

    建立新的本地端 Repository。

  • git clone [Repository URL]

    複製遠端的 Repository 檔案到本地端。

  • git status

    檢查本地端檔案異動狀態。

  • git add [檔案或資料夾]

    將指定的檔案(或資料夾)加入版本控制。用 git add . 可加入全部。

  • git commit

    提交(commit)目前的異動。

  • git commit -m "提交說明內容"

    提交(commit)目前的異動並透過 -m 參數設定摘要說明文字。

  • git stash

    獲取目前工作目錄的 dirty state,並保存到一個未完成變更的 stack,以方便隨時回復至當初的 state。

  • git log

    查看先前的 commit 記錄。

  • git push

    將本地端 Repository 的 commit 發佈到遠端。

  • git push origin [BRANCH_NAME]

    發佈至遠端指定的分支(Branch)

  • git branch

    查看分支。

  • git branch [BRANCH_NAME]

    建立分支。

  • git checkout [BRANCH_NAME]

    取出指定的分支。

  • git checkout -b [BRANCH_NAME]

    建立並跳到該分支。

  • git branch -D [BRANCH_NAME]

    強制刪除指定分支(須先切換至其他分支再做刪除)。

  • git reset --hard [HASH]

    強制恢復到指定的 commit(透過 Hash 值)。

  • git checkout [HASH]

    切換到指定的 commit(與 git checkout [BRANCH_NAME] 相同)。

  • git branch -m <OLD_BRANCH_NAME> <NEW_BRANCH_NAME>

    修改分支名稱。


Git 指令表

config

GitzshdoRemark
git config --list查看設定
git config --local user.name "(userName)"設定帳號
git config --local user.email "(e-mail)"設定E-mail全域
git config --global user.name "(userName)"設定帳號單專案
git config --global user.email "(e-mail)"設定E-mail單專案

init / clone

GitzshdoRemark
git clone抓遠端儲存庫下來
git initGit 初始化
rm -rf .git移除 Git

remote

GitzshdoRemark
git remote add (origin) (git@~.git)遠端連結
git remote set-url (origin) (git@~.git)-修改遠端連結
git remote remove (origin)-移除遠端連結
git remote -v查詢遠端連結(URL)
git push -u (origin) (master)推上遠端並綁定

基本版更( pull / push / add / commit / status)

GitzshdoRemark
git statusgst
git add (file)ga (~)
git add .ga .
git commit -m'message'gcmsg '(~)'
git pullgl
git push (remote) (branch)gp () ()
git push -u (remote) (branch)
git restore --staged (file)取消 git add
git pull --rebase (remote) (branch)gl

檔案變更版更操作

GitzshdoRemark
git clean -fd-清除未被追蹤的所有檔案已編輯的會恢復,新增的不會變動
git checkout (file)-當前目錄回復前次存檔(已編輯的會恢復,新增的不會變動)
git restore (file)-當前目錄回復前次存檔(含被刪除的檔)

Branch 分支應用

GitzshdoRemark
git branch-查詢所有本地分支
git branch -a-查詢所有遠端分支
git branch (newBranch)-當前 commit 新建分支
git branch (newBranch) (commitID)-特定 commit 上新建分支
git checkout (branch)-切換到某分支
git checkout -b (newBranch)-新建分支並切換過去
git branch -d (branch)-刪除某分支
git branch -D (branch)-強制刪除某分支
git branch -m (branch) (newName)-將某 branch 更名必須先切到不同分支

Reset 切到某版本

GitzshdoRemark
git reset (commit)預設為'mixed'
git reset (commit) --mixed放回"1-工作目錄"
git reset (commit) --soft放回"2-暫存區"
git reset (commit) --hard都不留(直接被隱藏)
git reset (commit)^退回前1次的commit^^ 退回前2版…
git reset (commit)~5退回前5次的commit~N 退至前N版
  • (commit)可以是 branch / commit ID / HEAD

Rebase & Merge 合併應用

GitzshdoRemark
git rebase (branch)重接分支基底
git merge (branch)合併分支(平行)

查詢

GitzshdoRemark
git config --list查詢目前設定
which git查詢 Git 位置
git --version查詢Git版本
git status查詢狀態
git log查詢 Log
git log --oneline查詢 Log(單行顯示)
git log --oneline --all --graph樹狀顯示 Log
git log -p FileName查詢檔案 Log
git blame FileName查詢該檔案每行編輯資訊(上傳者&時間)
git reflog查詢 reflog
git help查詢指令
  • reflog:reflog 保留HEAD移動的軌跡,可以查詢到commit ID(用於尋找被隱藏的 commit 

切換到新分支

git checkout new_branch

建立並切換到新分支

git checkout -b new_branch

將本地分支推送到遠端,使用 git push 命令,並指定要推送的分支名稱和遠端的分支名稱 git push -u origin <branch-name>

git push -u origin new_branch

但一般主要的遠端數據庫我們都會把它命名為`` origin,其他特殊作用的遠端數據庫才會刻意命名,後面的操作就都會是以這一個變數名稱為主,接下來執行以下命令:

說明:

  • git push:將本地指定分支推送至遠端數據庫
  • -u:同 --set-upstream,設定推送分支的上游
  • origin:要推向哪個遠端數據庫,寫名稱即可 (就是指前面說的 origin)
  • master:指定本地 master 分支進行推送,如果存在 master 分支即合併,不存在即新增

這一段可能會比較不好理解,讓我們將上面這段命令完整的寫出來:

git push --set-upstream origin master:master

複製

首先是 -u 的部分,等同於 --set-upstream,可以使 master 這一個指定的分支開始追蹤遠端的分支,只要做過一次 git push -u origin master,並且成功 Push 出去,本地的 master 分支就會被設定去追蹤遠端的 origin/master 分支,往後再 master 分支直接使用 git push 命令就會推向當時設定的 origin/master 分支,反之,如果沒有設定 -u 就使用 git push,就會導致以下錯誤:

可能有人會想,那我是否可以在不設定 -u 的情況下使用以下指令呢?

git push origin master

複製

答案是可以的,我們為什麼要設定 -u 就是要方便往後在直接使用 git push 命令時,Git 能夠知道此命令該推向何處,上面這種寫法明確的定義推向何處,結果與 git push -u origin master 一樣,只是我們習慣在第一次推送時,在明確定義該推向何處時,同時也設置往後這個位置就是預設推向的位置,有關 -u 的設定一樣可以到 /.git/config 尋找:

[branch "master"]
    remote = origin
    merge = refs/heads/master

刪除的本地分支

git branch -D branch_to_delete

刪除遠端分支

git push origin --delete branch_to_delete

git 誤刪分支恢復方法

# 建立分支 abc
git branch abc

# 切換分支
git checkout abc

# 立一個檔案  & commit & push
echo 'abc' > test.txt &&
git add . &&
git commit -m 'add test.txt' &&
git push -u origin abc


# 刪除分支abc
git checkout master
git branch -D abc

# git br查看分支列表,abc分支已不存在
git branch -a

# 使用git log -g 找回之前提交的commit
commit 3eac14d05bc1264cda54a7c21f04c3892f32406a
Reflog: HEAD@{1} (fdipzone <fdipzone@sina.com>)
Reflog message: commit: add test.txt
Author: fdipzone <fdipzone@sina.com>
Date:   Sun Jan 31 22:26:33 2016 +0800

    add test.txt
    
    
# git branch recover_branch[新分支] commit_id命令用這個commit建立一個分支
git branch abc 3eac14d05bc1264cda54a7c21f04c3892f32406a

# 可以見到recover_branch_abc已建立
git branch -a


# 切換到recover_branch_abc分支,檢查檔案是否存在
git checkout recover_branch_abc